@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.
- 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/consequence.mjs +29 -0
- package/lib/control.mjs +1052 -91
- package/lib/csd.mjs +17 -2
- package/lib/dataflow.mjs +245 -78
- 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 +85 -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 +4 -1
- package/lib/exploitability.mjs +263 -0
- package/lib/ironwork.mjs +111 -0
- package/lib/kernel/findings.mjs +11 -0
- package/lib/kernel/registry.mjs +4 -0
- package/lib/option-diff.mjs +49 -0
- package/lib/options.mjs +109 -46
- package/lib/parser.mjs +29 -7
- package/lib/policy.mjs +8 -3
- package/lib/precompile.mjs +4 -0
- package/lib/reach.mjs +29 -12
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +1 -0
- package/lib/sbom.mjs +147 -0
- package/lib/scan.mjs +93 -27
- package/lib/sets/cics.mjs +114 -0
- package/lib/sets/flow.mjs +82 -2
- package/lib/sets/jcl.mjs +67 -0
- package/lib/sets/semantics.mjs +377 -0
- package/lib/sets/web.mjs +34 -10
- package/lib/sets/zowe.mjs +220 -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 +85 -13
- 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 +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,
|
|
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
|
|
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
|
|
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
|
|
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) ? [] :
|
|
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
|
|
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.
|
|
117
|
-
// false where a required check is not generated and null where no one can tell; `forbidden`
|
|
118
|
-
// options the policy's forbid list names. `generated` maps each program and build script to
|
|
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
|
-
|
|
123
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
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 =
|
|
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: [],
|
|
174
|
-
|
|
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
|
-
|
|
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, ...(
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
package/lib/capabilities.mjs
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
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: {
|
package/lib/consequence.mjs
CHANGED
|
@@ -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.
|