@portll/cobolwork 0.2.117 → 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/lib/sets/zowe.mjs CHANGED
@@ -54,9 +54,9 @@ export const ZOWE_RULES = {
54
54
  },
55
55
  'zowe-mcp-unpinned': {
56
56
  sev: 'low', evidence: 'construct', cwe: 'CWE-829',
57
- text: 'A Zowe MCP server is launched by npx with no pinned version',
57
+ text: 'A Zowe MCP server is launched from the registry with no pinned version',
58
58
  impact: 'Whatever version the registry serves next runs with the agent\'s mainframe credentials',
59
- remedy: 'Pin the version: npx @zowe/mcp-server@<version>',
59
+ remedy: 'Pin the version: @zowe/mcp-server@<version>',
60
60
  },
61
61
  'zowe-mock-credentials': {
62
62
  sev: 'med', evidence: 'construct', cwe: 'CWE-798',
@@ -81,7 +81,12 @@ function stripJsonc(text) {
81
81
  else if (c === '/' && text[i + 1] === '*') { i += 2; while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) { if (text[i] === '\n') out += '\n'; i++; } i++; }
82
82
  else if (c === ',') {
83
83
  let j = i + 1;
84
- while (j < text.length && /\s/.test(text[j])) j++;
84
+ for (;;) {
85
+ while (j < text.length && /\s/.test(text[j])) j++;
86
+ if (text[j] === '/' && text[j + 1] === '/') { while (j < text.length && text[j] !== '\n') j++; }
87
+ else if (text[j] === '/' && text[j + 1] === '*') { j += 2; while (j < text.length && !(text[j] === '*' && text[j + 1] === '/')) j++; j += 2; }
88
+ else break;
89
+ }
85
90
  if (text[j] !== '}' && text[j] !== ']') out += c;
86
91
  } else out += c;
87
92
  }
@@ -109,6 +114,7 @@ function kindOf(rel) {
109
114
  if (base === 'settings.json' && parent === '.gemini') return 'mcp';
110
115
  if (base === 'settings.json' && parent === '.vscode') return 'vscode';
111
116
  if (base === 'systems.json' && parts.slice(0, -1).some((p) => /mock/i.test(p))) return 'mock';
117
+ if (base === 'native-config.json') return 'native';
112
118
  return null;
113
119
  }
114
120
 
@@ -138,7 +144,10 @@ function teamConfig(config, raw, rel, findings) {
138
144
  });
139
145
  }
140
146
 
141
- const isZoweServer = (s) => s && typeof s === 'object' && /@zowe\/mcp-server|zowe-mcp/.test([s.command, ...(Array.isArray(s.args) ? s.args : [])].map(String).join(' '));
147
+ // The Zowe MCP server by its package or command, or by the ZOWE_MCP_ variables only it reads.
148
+ const isZoweServer = (s) => s && typeof s === 'object' && (/@zowe\/mcp-server|zowe-mcp/.test([s.command, ...(Array.isArray(s.args) ? s.args : [])].map(String).join(' ')) || Object.keys(s.env && typeof s.env === 'object' ? s.env : {}).some((k) => k.startsWith('ZOWE_MCP_')));
149
+ // Commands that run whatever version of a package the registry serves.
150
+ const REGISTRY_RUNNERS = new Set(['npx', 'bunx', 'pnpx', 'pnpm', 'yarn', 'npm']);
142
151
 
143
152
  function tierOf(server) {
144
153
  const args = Array.isArray(server.args) ? server.args.map(String) : [];
@@ -170,9 +179,9 @@ function mcpConfig(config, raw, rel, findings) {
170
179
  const t = tierFinding(tierOf(server), rel, lineOf(raw, 'capability-tier', name) || lineOf(raw, 'ZOWE_MCP_CAPABILITY_TIER', name), `server ${name}`);
171
180
  if (t) findings.push(t);
172
181
  if (String(env.ZOWE_MCP_DATA_MARKING) === '0') findings.push({ rule: 'zowe-mcp-data-marking-off', path: rel, line: lineOf(raw, 'ZOWE_MCP_DATA_MARKING', name), detail: `server ${name} sets ZOWE_MCP_DATA_MARKING to 0` });
173
- if (String(server.command) === 'npx') {
182
+ if (REGISTRY_RUNNERS.has(String(server.command))) {
174
183
  const pkg = (Array.isArray(server.args) ? server.args.map(String) : []).find((a) => a.startsWith('@zowe/mcp-server'));
175
- if (pkg && (pkg === '@zowe/mcp-server' || pkg === '@zowe/mcp-server@latest')) findings.push({ rule: 'zowe-mcp-unpinned', path: rel, line: lineOf(raw, pkg, name), detail: `server ${name} runs ${pkg} through npx` });
184
+ if (pkg && (pkg === '@zowe/mcp-server' || pkg === '@zowe/mcp-server@latest')) findings.push({ rule: 'zowe-mcp-unpinned', path: rel, line: lineOf(raw, pkg, name), detail: `server ${name} runs ${pkg} through ${server.command}` });
176
185
  }
177
186
  }
178
187
  }
@@ -188,7 +197,7 @@ function passwordPaths(value, path, out) {
188
197
  else if (value && typeof value === 'object') {
189
198
  for (const [k, v] of Object.entries(value)) {
190
199
  const p = path ? `${path}.${k}` : k;
191
- if (/^password$/i.test(k) && typeof v === 'string' && v) out.push(p);
200
+ if (/^password$/i.test(k) && ((typeof v === 'string' && v && !isReference(v)) || typeof v === 'number')) out.push(p);
192
201
  else passwordPaths(v, p, out);
193
202
  }
194
203
  }
@@ -199,7 +208,24 @@ function mockSystems(config, raw, rel, findings) {
199
208
  for (const p of passwordPaths(config, '', [])) findings.push({ rule: 'zowe-mock-credentials', path: rel, line: lineOf(raw, 'password'), detail: `the mock systems file holds a password at ${p}` });
200
209
  }
201
210
 
202
- const READERS = { team: teamConfig, mcp: mcpConfig, vscode: vscodeSettings, mock: mockSystems };
211
+ // The server's own configuration, read without assuming its layout: a literal password, and TLS
212
+ // verification turned off, wherever they are.
213
+ function nativeConfig(config, raw, rel, findings) {
214
+ for (const p of passwordPaths(config, '', [])) findings.push({ rule: 'zowe-config-secret-in-clear', path: rel, line: lineOf(raw, 'password'), detail: `the Zowe MCP server configuration holds a password at ${p}` });
215
+ const walk = (v, path) => {
216
+ if (Array.isArray(v)) v.forEach((x, i) => walk(x, `${path}[${i}]`));
217
+ else if (v && typeof v === 'object') {
218
+ for (const [k, x] of Object.entries(v)) {
219
+ const p = path ? `${path}.${k}` : k;
220
+ if (k === 'rejectUnauthorized' && x === false) findings.push({ rule: 'zowe-config-tls-verify-off', path: rel, line: lineOf(raw, 'rejectUnauthorized'), detail: `the Zowe MCP server configuration turns TLS verification off at ${p}` });
221
+ else walk(x, p);
222
+ }
223
+ }
224
+ };
225
+ walk(config, '');
226
+ }
227
+
228
+ const READERS = { team: teamConfig, mcp: mcpConfig, vscode: vscodeSettings, mock: mockSystems, native: nativeConfig };
203
229
 
204
230
  export function scanZowe(root, opts = {}) {
205
231
  const tree = treeFor(root, opts);
package/lib/utilities.mjs CHANGED
@@ -8,6 +8,9 @@
8
8
  // statements are not in the job - SYSIN naming a library member - says so, because its copies are
9
9
  // then only the ones its DD names decide on their own.
10
10
 
11
+ import { ftpSession } from './ftp.mjs';
12
+ import { statementCard } from './card.mjs';
13
+
11
14
  const upper = (s) => String(s).toUpperCase();
12
15
 
13
16
  // The DDs of a step by name, each followed by the datasets concatenated to it.
@@ -93,7 +96,7 @@ function commands(lines) {
93
96
  let comment = false;
94
97
  for (const { line, text } of lines) {
95
98
  let t = '';
96
- const s = text.slice(0, 72);
99
+ const s = statementCard(text).text;
97
100
  for (let i = 0; i < s.length; i++) {
98
101
  if (comment) { if (s[i] === '*' && s[i + 1] === '/') { comment = false; i++; } continue; }
99
102
  if (s[i] === '/' && s[i + 1] === '*') { comment = true; i++; continue; }
@@ -189,11 +192,42 @@ function sortCopies(step, dds) {
189
192
  // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i211.htm
190
193
  // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/import.htm
191
194
  // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2221.htm
195
+ // PRINT reads INFILE(ddname) or INDATASET(entryname) and writes the listing to OUTFILE(ddname),
196
+ // SYSPRINT when it names none, so a production data set printed to SYSOUT leaves by that DD.
197
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/print.htm
198
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2244.htm
199
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2245.htm
200
+ // ALTER entryname NEWNAME(newname), abbreviated NEWNM, gives the entry a new name, so the data the
201
+ // old name held is read under the new one. A generic name (both must then be generic) or a member
202
+ // of a partitioned data set leaves the data set where it was, and is not followed.
203
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2052.htm
204
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2055.htm
205
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2056.htm
206
+ function renamed(cmd) {
207
+ const m = /^ALTER\s+'?([^\s(),'*]+)'?(?=[\s,]|$)/i.exec(cmd.text);
208
+ const to = m && param(cmd.text, ['NEWNAME', 'NEWNM']);
209
+ if (!to || /[*(]/.test(to[0])) return null;
210
+ return { from: [{ dsn: upper(m[1]) }], to: [{ dsn: to[0] }], line: cmd.line };
211
+ }
212
+
192
213
  function reproCopies(step, dds) {
193
214
  const { lines, unread } = cards(dds.get('SYSIN'));
194
- const notes = unread ? [{ line: step.line, text: `${who(step)} runs IDCAMS with commands in ${unread}, which this reader cannot see, so any REPRO, EXPORT or IMPORT it runs is not known` }] : [];
215
+ const notes = unread ? [{ line: step.line, text: `${who(step)} runs IDCAMS with commands in ${unread}, which this reader cannot see, so any REPRO, EXPORT, IMPORT, PRINT or rename it runs is not known` }] : [];
195
216
  const moves = [];
196
217
  for (const cmd of commands(lines)) {
218
+ if (/^ALTER\b/i.test(cmd.text)) {
219
+ const move = renamed(cmd);
220
+ if (move) moves.push(move);
221
+ continue;
222
+ }
223
+ if (/^PRINT\b/i.test(cmd.text)) {
224
+ const input = param(cmd.text, ['INFILE', 'IFILE']);
225
+ const ds = !input && param(cmd.text, ['INDATASET', 'IDS']);
226
+ const out = param(cmd.text, ['OUTFILE', 'OFILE']);
227
+ const from = input ? [{ dd: input[0] }] : ds ? [{ dsn: ds[0] }] : [];
228
+ moves.push({ from, to: [{ dd: out ? out[0] : 'SYSPRINT' }], line: cmd.line });
229
+ continue;
230
+ }
197
231
  const verb = /^(REPRO|EXPORT|EXP|IMPORT)\b/i.exec(cmd.text);
198
232
  if (!verb) continue;
199
233
  const entry = /^EXP/i.test(verb[1]) && /^\S+\s+([^\s(),]+)/.exec(cmd.text);
@@ -293,7 +327,7 @@ function icetoolCopies(step, dds) {
293
327
  const statements = [];
294
328
  let cur = null;
295
329
  for (const { line, text } of lines) {
296
- const s = text.slice(0, 72);
330
+ const s = statementCard(text).text;
297
331
  if (!cur && (/^\*/.test(s) || !s.trim())) continue;
298
332
  const tokens = s.trim().split(/\s+/);
299
333
  const hyphen = tokens.findIndex((t) => t.endsWith('-'));
@@ -321,6 +355,21 @@ function icetoolCopies(step, dds) {
321
355
  return { moves, notes };
322
356
  }
323
357
 
358
+ // An FTP step's transfers, from the session lib/ftp.mjs reads: what PUt, MPut and APpend send leaves
359
+ // for the remote host, and what Get and MGet fetch is written locally. The remote end names the host
360
+ // and file, and the FILETYPE a SIte gave it.
361
+ function ftpCopies(step, dds) {
362
+ const s = ftpSession(step, dds);
363
+ const localEnds = (x) => (x.dd ? [{ dd: x.dd }] : x.dsns.map((dsn) => ({ dsn })));
364
+ const remote = (name, x) => ({ remote: { host: s.host, name, ...(x.filetype ? { filetype: x.filetype } : {}) } });
365
+ const moves = [
366
+ ...s.sends.map((x) => ({ from: localEnds(x), to: [remote(x.foreign, x)], line: x.line })),
367
+ ...s.gets.map((x) => ({ from: [remote(x.name, x)], to: localEnds(x), line: x.line })),
368
+ ].sort((a, b) => a.line - b.line);
369
+ const notes = s.inputUnread ? [{ line: step.line, text: `${who(step)} runs FTP with subcommands in ${s.inputUnread}, which this reader cannot see, so what it sends or fetches is not known` }] : [];
370
+ return { moves, notes };
371
+ }
372
+
324
373
  const TABLE = [
325
374
  [['IEBGENER', 'ICEGENER', 'IEBPTPCH', 'IEBUPDTE'], generCopy],
326
375
  [['SORT', 'ICEMAN', 'DFSORT', 'SYNCSORT'], sortCopies],
@@ -328,6 +377,7 @@ const TABLE = [
328
377
  [['IEBCOPY'], iebcopyCopies],
329
378
  [['ADRDSSU'], dssCopies],
330
379
  [['ICETOOL'], icetoolCopies],
380
+ [['FTP'], ftpCopies],
331
381
  ];
332
382
  const BY_PROGRAM = new Map(TABLE.flatMap(([names, fn]) => names.map((n) => [n, fn])));
333
383
 
@@ -346,7 +396,7 @@ export function copiesOf(step) {
346
396
  // An end naming a DD stands for every dataset concatenated under that name. A DD the step does
347
397
  // not define is kept, with no dataset: a statement naming it still says where the data went.
348
398
  const expand = (end) => {
349
- if (!end.dd) return [{ dd: null, dsn: end.dsn }];
399
+ if (!end.dd) return [{ dd: null, dsn: end.dsn ?? null, ...(end.remote ? { remote: end.remote } : {}) }];
350
400
  const group = dds.get(end.dd);
351
401
  if (!group) return [{ dd: end.dd, dsn: null }];
352
402
  return group.map((dd) => ({ dd: end.dd, dsn: dd.dsn || null, ...(dd.inStream ? { inStream: true } : {}) }));
@@ -366,3 +416,95 @@ export function copiesOf(step) {
366
416
  }
367
417
  return { copies, uses, notes };
368
418
  }
419
+
420
+ // A TSO command's operands: KEYWORD(value) with the value's parentheses and quotes kept, a quoted
421
+ // string, or a bare word, split on blanks and commas. Blanks may stand between a keyword and its
422
+ // parenthesis, as in PROGRAM (GETTAB).
423
+ function tsoOperands(text) {
424
+ const out = [];
425
+ let i = 0;
426
+ while (i < text.length) {
427
+ if (/[\s,]/.test(text[i])) { i++; continue; }
428
+ const start = i;
429
+ let depth = 0, quoted = false;
430
+ for (; i < text.length; i++) {
431
+ const c = text[i];
432
+ if (c === "'") quoted = !quoted;
433
+ else if (quoted) continue;
434
+ else if (c === '(') depth++;
435
+ else if (c === ')') depth = Math.max(0, depth - 1);
436
+ else if (depth === 0 && /[\s,]/.test(c)) {
437
+ const next = /^\s*\(/.exec(text.slice(i));
438
+ if (next && /^[A-Z][A-Z0-9$#@]*$/i.test(text.slice(start, i))) { text = text.slice(0, i) + text.slice(i + next[0].length - 1); i--; continue; }
439
+ break;
440
+ }
441
+ }
442
+ const tok = text.slice(start, i);
443
+ const kw = /^([A-Z][A-Z0-9$#@]*)\((.*)\)$/is.exec(tok);
444
+ out.push(kw ? { key: upper(kw[1]), value: kw[2] } : { word: tok });
445
+ }
446
+ return out;
447
+ }
448
+
449
+ const unquote = (s) => s.replace(/^'(.*)'$/s, '$1').replace(/''/g, "'");
450
+
451
+ // ALLOCATE's keywords, abbreviated as far as IBM's rule allows: to any leading part that no other
452
+ // ALLOCATE keyword shares. FILE shares F, FI and FIL with FILEDATA, FCB, FLASH and FORMS, and DSNAME
453
+ // shares DS and DSN with DSNTYPE and DSORG.
454
+ const ALLOC_DATASET = /^(DA|DAT|DATA|DATAS|DATASE|DATASET|DSNA|DSNAM|DSNAME)$/;
455
+ const ALLOC_FILE = /^(FILE|DD|DDN|DDNA|DDNAM|DDNAME)$/;
456
+ const ALLOC_STATUS = new Set(['OLD', 'SHR', 'MOD', 'NEW']);
457
+
458
+ // What a TSO batch step (IKJEFT01, IKJEFT1A, IKJEFT1B) does in its commands, from the step's PARM,
459
+ // which TSO runs as its first command, and the in-stream SYSTSIN:
460
+ // allocations: ALLOCATE's DDs, which the programs the step runs open as if the JCL defined them.
461
+ // A data set name in quotes is the whole name; one without has the user's prefix added, which
462
+ // the job does not say, so it is kept as written with no dsn.
463
+ // runs: the programs the step starts, by TSO CALL, whose parameter string reaches the program as
464
+ // EXEC PARM does, and by the DSN subcommand RUN PROGRAM, with PARMS.
465
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.1.0/com.ibm.zos.v2r1.ikjc500/allocsyn.htm
466
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_3.2.0/com.ibm.zos.v3r2.ikjc500/alloccmd.htm
467
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.ikjp100/ikjp100114.htm
468
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.5.0/com.ibm.zos.v2r5.ikjc200/ikj2o20039.htm
469
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.ikjc500/ikj2l2_CALL_command_operands.htm
470
+ // https://www.ibm.com/docs/api/v1/content/SSEPEK_13.0.0/comref/src/tpc/db2z_cmd_run.html
471
+ export const TSO_BATCH = new Set(['IKJEFT01', 'IKJEFT1A', 'IKJEFT1B']);
472
+
473
+ export function tsoCommands(step) {
474
+ const out = { allocations: [], runs: [], notes: [] };
475
+ if (!step.pgm || !TSO_BATCH.has(upper(step.pgm))) return out;
476
+ const { lines, unread } = cards(ddGroups(step).get('SYSTSIN'));
477
+ if (unread) out.notes.push({ line: step.line, text: `${who(step)} runs TSO with commands in ${unread}, which this reader cannot see, so any ALLOCATE, CALL or RUN it runs is not known` });
478
+ // PARM keeps JCL's doubled apostrophes; TSO sees one.
479
+ const cmds = [...(step.parm ? [{ line: step.line, text: step.parm.replace(/''/g, "'") }] : []), ...commands(lines)];
480
+ for (const cmd of cmds) {
481
+ const verb = upper(/^\S*/.exec(cmd.text)[0]);
482
+ const ops = tsoOperands(cmd.text.slice(verb.length));
483
+ if (verb === 'ALLOC' || verb === 'ALLOCATE') {
484
+ const file = ops.find((o) => o.key && ALLOC_FILE.test(o.key));
485
+ if (!file) continue;
486
+ const names = ops.find((o) => o.key && ALLOC_DATASET.test(o.key));
487
+ const words = ops.filter((o) => o.word).map((o) => upper(o.word));
488
+ const sysout = ops.find((o) => o.key === 'SYSOUT') || (words.includes('SYSOUT') ? { value: '' } : null);
489
+ const datasets = names ? tsoOperands(names.value).map((o) => o.word).filter((w) => w && w !== '*') : [];
490
+ if (!datasets.length && !sysout) continue;
491
+ out.allocations.push({
492
+ dd: upper(unquote(file.value)), line: cmd.line,
493
+ status: words.find((w) => ALLOC_STATUS.has(w)) || null,
494
+ sysout: sysout ? upper(sysout.value) || '*' : null,
495
+ datasets: datasets.map((w) => ({ written: w, dsn: /^'.*'$/.test(w) ? upper(unquote(w)) : null })),
496
+ });
497
+ } else if (verb === 'CALL') {
498
+ const target = ops[0]?.word || (ops[0]?.key ? `${ops[0].key}(${ops[0].value})` : '');
499
+ const member = /\(([A-Z0-9$#@]{1,8})\)'?$/i.exec(target);
500
+ const parm = ops[1]?.word && /^'.*'$/s.test(ops[1].word) ? unquote(ops[1].word) : null;
501
+ out.runs.push({ program: member ? upper(member[1]) : 'TEMPNAME', parm, line: cmd.line, via: 'TSO CALL' });
502
+ } else if (verb === 'RUN') {
503
+ const program = ops.find((o) => o.key === 'PROGRAM');
504
+ if (!program || !/^[A-Z0-9$#@]{1,8}$/i.test(program.value.trim())) continue;
505
+ const parms = ops.find((o) => o.key === 'PARMS');
506
+ out.runs.push({ program: upper(program.value.trim()), parm: parms ? unquote(parms.value.trim()) : null, line: cmd.line, via: 'DSN RUN' });
507
+ }
508
+ }
509
+ return out;
510
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portll/cobolwork",
3
- "version": "0.2.117",
3
+ "version": "0.2.140",
4
4
  "description": "COBOL, JCL and CICS security analysis with zero runtime dependencies: reference-format parser, cross-program data flow, mainframe credential rules. Plugs into commitwork.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "author": "Portll <john@portll.net>",
@@ -2634,7 +2634,7 @@
2634
2634
  "framework": "dora",
2635
2635
  "clause": "Article 8(2)",
2636
2636
  "title": "Identification",
2637
- "rationale": "The rule reports: A Zowe MCP server is launched by npx with no pinned version. That is a source of ICT risk in a business-supporting function, identified by reading the source rather than by asking the team that maintains it.",
2637
+ "rationale": "The rule reports: A Zowe MCP server is launched from the registry with no pinned version. That is a source of ICT risk in a business-supporting function, identified by reading the source rather than by asking the team that maintains it.",
2638
2638
  "source": {
2639
2639
  "doc": "dora",
2640
2640
  "retrieved": "2026-09-20",
@@ -2617,7 +2617,7 @@
2617
2617
  "framework": "ffiec",
2618
2618
  "clause": "DA&M IV",
2619
2619
  "title": "Common Development, Acquisition, and Maintenance Practices",
2620
- "rationale": "The rule reports: A Zowe MCP server is launched by npx with no pinned version. That is a component or dependency the inventory does not accurately account for, which this practice requires be catalogued.",
2620
+ "rationale": "The rule reports: A Zowe MCP server is launched from the registry with no pinned version. That is a component or dependency the inventory does not accurately account for, which this practice requires be catalogued.",
2621
2621
  "source": {
2622
2622
  "doc": "ffiec",
2623
2623
  "retrieved": "2026-09-20",
@@ -2617,7 +2617,7 @@
2617
2617
  "framework": "nist-800-53r5",
2618
2618
  "clause": "CM-8",
2619
2619
  "title": "System Component Inventory",
2620
- "rationale": "The rule reports: A Zowe MCP server is launched by npx with no pinned version. That is a component the inventory does not accurately account for, which this control requires be documented.",
2620
+ "rationale": "The rule reports: A Zowe MCP server is launched from the registry with no pinned version. That is a component the inventory does not accurately account for, which this control requires be documented.",
2621
2621
  "source": {
2622
2622
  "doc": "nist80053",
2623
2623
  "retrieved": "2026-09-20",
@@ -5,35 +5,83 @@
5
5
  "description": "Which findings stop a build, which run-time checks the compiler must generate, and what an incomplete scan means. docs/spec/build-gate.md section 4. lib/policy.mjs validates the same shape and also refuses a rule id cobolwork does not declare.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["policyVersion"],
8
+ "required": [
9
+ "policyVersion"
10
+ ],
9
11
  "properties": {
10
- "_comment": { "type": "string" },
11
- "policyVersion": { "const": 1 },
12
- "block": { "$ref": "#/$defs/tier", "description": "A finding at or above this tier blocks. Default high." },
13
- "always": { "$ref": "#/$defs/tier", "description": "In ratchet mode, a finding at or above this tier blocks whether or not the change introduced it. Default crit." },
12
+ "_comment": {
13
+ "type": "string"
14
+ },
15
+ "policyVersion": {
16
+ "const": 1
17
+ },
18
+ "block": {
19
+ "$ref": "#/$defs/tier",
20
+ "description": "A finding at or above this tier blocks. Default high."
21
+ },
22
+ "always": {
23
+ "$ref": "#/$defs/tier",
24
+ "description": "In ratchet mode, a finding at or above this tier blocks whether or not the change introduced it. Default crit."
25
+ },
14
26
  "classes": {
15
27
  "type": "array",
16
- "items": { "enum": ["privilege-escalation", "data-mutation"] },
28
+ "items": {
29
+ "enum": [
30
+ "privilege-escalation",
31
+ "data-mutation"
32
+ ]
33
+ },
17
34
  "uniqueItems": true,
18
35
  "description": "A finding in one of these consequence classes blocks at any tier. Default both."
19
36
  },
20
37
  "rules": {
21
38
  "type": "object",
22
- "additionalProperties": { "enum": ["block", "warn"] },
39
+ "additionalProperties": {
40
+ "enum": [
41
+ "block",
42
+ "warn"
43
+ ]
44
+ },
23
45
  "description": "Per rule id: block whatever its tier, or warn whatever its tier or class."
24
46
  },
25
- "coverage": { "enum": ["block", "warn"], "description": "Incomplete coverage makes the build undecided (block) or is reported (warn). Default block." },
26
- "options": { "enum": ["block", "warn"], "description": "A build without a required run-time check fails, and options nothing declares make it undecided (block); or both are reported and the build passes relaxed (warn). An option forbid names, and a check the change removes, fail either way. Default warn." },
47
+ "coverage": {
48
+ "enum": [
49
+ "block",
50
+ "warn"
51
+ ],
52
+ "description": "Incomplete coverage makes the build undecided (block) or is reported (warn). Default block."
53
+ },
54
+ "options": {
55
+ "enum": [
56
+ "block",
57
+ "warn"
58
+ ],
59
+ "description": "A build without a required run-time check fails, and options nothing declares make it undecided (block); or both are reported and the build passes relaxed (warn). An option forbid names, and a check the change removes, fail either way. Default warn."
60
+ },
27
61
  "waivers": {
28
62
  "type": "object",
29
63
  "additionalProperties": false,
30
- "required": ["maxDays"],
31
- "properties": { "maxDays": { "type": "integer", "minimum": 1 } },
64
+ "required": [
65
+ "maxDays"
66
+ ],
67
+ "properties": {
68
+ "maxDays": {
69
+ "type": "integer",
70
+ "minimum": 1
71
+ }
72
+ },
32
73
  "description": "A waiver written with an expiry further off than maxDays covers nothing. Default 180."
33
74
  },
34
75
  "checks": {
35
76
  "type": "array",
36
- "items": { "enum": ["subscript", "reference-modification", "numeric-data", "argument-length"] },
77
+ "items": {
78
+ "enum": [
79
+ "subscript",
80
+ "reference-modification",
81
+ "numeric-data",
82
+ "argument-length"
83
+ ]
84
+ },
37
85
  "uniqueItems": true,
38
86
  "description": "The run-time checks every program must be compiled with. Default subscript and reference-modification."
39
87
  },
@@ -41,15 +89,44 @@
41
89
  "type": "object",
42
90
  "additionalProperties": false,
43
91
  "properties": {
44
- "enterprise": { "$ref": "#/$defs/options" },
45
- "gnucobol": { "$ref": "#/$defs/options" },
46
- "gcobol": { "$ref": "#/$defs/options" }
92
+ "enterprise": {
93
+ "$ref": "#/$defs/options"
94
+ },
95
+ "gnucobol": {
96
+ "$ref": "#/$defs/options"
97
+ },
98
+ "gcobol": {
99
+ "$ref": "#/$defs/options"
100
+ }
47
101
  },
48
102
  "description": "Options a build must not pass, beyond those that turn a required check off."
103
+ },
104
+ "requireEquivalence": {
105
+ "enum": [
106
+ "never",
107
+ "machineAuthored",
108
+ "always"
109
+ ],
110
+ "description": "When every program a change edits, or whose copybooks it edits, needs an ironwork equivalence statement passed to build --equivalence: never, only when a commit in the change names a code assistant or bot as author, committer or co-author (machineAuthored), or always. The stricter of the floor and the repository applies. Default never."
49
111
  }
50
112
  },
51
113
  "$defs": {
52
- "tier": { "enum": ["info", "low", "med", "high", "crit", "known-exploitable"] },
53
- "options": { "type": "array", "items": { "type": "string", "minLength": 1 } }
114
+ "tier": {
115
+ "enum": [
116
+ "info",
117
+ "low",
118
+ "med",
119
+ "high",
120
+ "crit",
121
+ "known-exploitable"
122
+ ]
123
+ },
124
+ "options": {
125
+ "type": "array",
126
+ "items": {
127
+ "type": "string",
128
+ "minLength": 1
129
+ }
130
+ }
54
131
  }
55
132
  }