@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/card.mjs +18 -0
- package/lib/cics-commands.mjs +136 -125
- package/lib/control.mjs +118 -1
- package/lib/dataflow.mjs +67 -84
- package/lib/embedded-sql.mjs +110 -0
- package/lib/evidence/journal.mjs +11 -2
- package/lib/explain.mjs +5 -4
- package/lib/ftp.mjs +136 -0
- package/lib/ironwork.mjs +4 -6
- package/lib/jcl.mjs +27 -7
- package/lib/layout.mjs +202 -0
- package/lib/parser.mjs +52 -203
- package/lib/precompile.mjs +5 -88
- package/lib/revision.json +1 -1
- package/lib/sbom.mjs +94 -6
- package/lib/sets/cics.mjs +5 -2
- package/lib/sets/copybook.mjs +2 -1
- package/lib/sets/hidden.mjs +4 -8
- package/lib/sets/jcl.mjs +16 -119
- package/lib/sets/recon.mjs +2 -1
- package/lib/sets/zowe.mjs +34 -8
- package/lib/utilities.mjs +146 -4
- package/package.json +1 -1
- package/rules/compliance-dora.json +1 -1
- package/rules/compliance-ffiec.json +1 -1
- package/rules/compliance-nist80053.json +1 -1
- package/schema/cobolwork.policy.schema.json +94 -17
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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": [
|
|
8
|
+
"required": [
|
|
9
|
+
"policyVersion"
|
|
10
|
+
],
|
|
9
11
|
"properties": {
|
|
10
|
-
"_comment": {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
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": {
|
|
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": {
|
|
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": {
|
|
26
|
-
|
|
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": [
|
|
31
|
-
|
|
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": {
|
|
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": {
|
|
45
|
-
|
|
46
|
-
|
|
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": {
|
|
53
|
-
|
|
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
|
}
|