@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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Db2 for z/OS CREATE VIEW, after the Db2 13 SQL Reference: the view's name and columns, its common
|
|
3
|
+
// table expressions and fullselect kept as tokens, the check option, and the names its FROM clauses read.
|
|
4
|
+
|
|
5
|
+
import { Db2Syntax } from '../cursor.mjs';
|
|
6
|
+
import { readName } from './table.mjs';
|
|
7
|
+
|
|
8
|
+
const isName = (t) => !!t && (t.t === 'word' || t.t === 'ident');
|
|
9
|
+
const isWord = (t, u) => !!t && t.t === 'word' && (Array.isArray(u) ? u.includes(t.u) : t.u === u);
|
|
10
|
+
const isOpen = (t) => !!t && t.t === 'op' && t.v === '(';
|
|
11
|
+
const isClose = (t) => !!t && t.t === 'op' && t.v === ')';
|
|
12
|
+
|
|
13
|
+
// What another dialect's view looks like at the token where a Db2 for z/OS one stops.
|
|
14
|
+
function hint(t, after) {
|
|
15
|
+
if (!t) return '';
|
|
16
|
+
if (t.t === 'op' && t.v === '/') return " ('/' qualifies names in Db2 for i system naming, not Db2 for z/OS)";
|
|
17
|
+
if (t.t !== 'word') return '';
|
|
18
|
+
if (t.u === 'IF' && (isWord(after, 'NOT') || isWord(after, 'EXISTS'))) return ' (IF [NOT] EXISTS is not Db2 for z/OS syntax)';
|
|
19
|
+
if (t.u === 'MATERIALIZED') return ' (Db2 for z/OS has materialized query tables, made by CREATE TABLE, not materialized views)';
|
|
20
|
+
if (t.u === 'READ' && isWord(after, 'ONLY')) return ' (WITH READ ONLY is Oracle, not Db2 for z/OS)';
|
|
21
|
+
if (t.u === 'CONSTRAINT') return ' (a named check option is Oracle, not Db2 for z/OS)';
|
|
22
|
+
if (t.u === 'WITH' && isOpen(after)) return ' (view options in parentheses are not Db2 for z/OS syntax)';
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function refuse(c, what) {
|
|
27
|
+
const t = c.peek();
|
|
28
|
+
throw new Db2Syntax(`expected ${what}${t ? ` at '${t.v ?? t.t}'` : ' at end of statement'}${hint(t, c.peek(1))}`, t || c.peek(-1));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function closing(toks, i) {
|
|
32
|
+
let depth = 0;
|
|
33
|
+
for (let k = i; k < toks.length; k++) {
|
|
34
|
+
if (isOpen(toks[k])) depth++;
|
|
35
|
+
else if (isClose(toks[k]) && --depth === 0) return k;
|
|
36
|
+
}
|
|
37
|
+
return toks.length;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const atTop = (toks, words) => {
|
|
41
|
+
let depth = 0;
|
|
42
|
+
return toks.some((t) => {
|
|
43
|
+
if (isOpen(t)) depth++;
|
|
44
|
+
else if (isClose(t)) depth--;
|
|
45
|
+
return depth === 0 && isWord(t, words);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// The words that end a FROM clause; FOR does too unless a period specification follows the table.
|
|
50
|
+
const FROM_END = ['WHERE', 'GROUP', 'HAVING', 'ORDER', 'FETCH', 'OFFSET', 'LIMIT', 'UNION', 'EXCEPT', 'INTERSECT', 'WITH', 'OPTIMIZE', 'QUERYNO', 'SKIP'];
|
|
51
|
+
|
|
52
|
+
// One FROM clause from i: a name that starts a table reference is recorded; TABLE (…), XMLTABLE (…) and
|
|
53
|
+
// the like are read only for the subqueries inside them. Returns the index where the clause ends.
|
|
54
|
+
function fromClause(toks, i, out) {
|
|
55
|
+
let start = true;
|
|
56
|
+
for (; i < toks.length; i++) {
|
|
57
|
+
const t = toks[i];
|
|
58
|
+
if (isWord(t, FROM_END) || (isWord(t, 'FOR') && !isWord(toks[i + 1], ['SYSTEM_TIME', 'BUSINESS_TIME']))) return i;
|
|
59
|
+
if ((t.t === 'op' && t.v === ',') || isWord(t, 'JOIN')) { start = true; continue; }
|
|
60
|
+
if (isOpen(t)) {
|
|
61
|
+
const j = closing(toks, i);
|
|
62
|
+
const inner = toks.slice(i + 1, j);
|
|
63
|
+
if (start && !atTop(inner, ['SELECT', 'UNION', 'EXCEPT', 'INTERSECT'])) fromClause(inner, 0, out);
|
|
64
|
+
else fromNames(inner, out);
|
|
65
|
+
i = j;
|
|
66
|
+
start = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!start) continue;
|
|
70
|
+
start = false;
|
|
71
|
+
if (!isName(t) || isWord(t, ['TABLE', 'FINAL', 'OLD']) || isOpen(toks[i + 1])) continue;
|
|
72
|
+
const parts = [t.v];
|
|
73
|
+
while (toks[i + 1] && toks[i + 1].t === 'op' && toks[i + 1].v === '.' && isName(toks[i + 2])) { parts.push(toks[i + 2].v); i += 2; }
|
|
74
|
+
out.push(parts.join('.'));
|
|
75
|
+
}
|
|
76
|
+
return i;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The names in the FROM clause of every subselect in toks, subqueries included; a FROM that no SELECT
|
|
80
|
+
// at its own depth precedes (EXTRACT (YEAR FROM D), IS DISTINCT FROM) is not one.
|
|
81
|
+
function fromNames(toks, out) {
|
|
82
|
+
let select = false;
|
|
83
|
+
for (let i = 0; i < toks.length; i++) {
|
|
84
|
+
const t = toks[i];
|
|
85
|
+
if (isOpen(t)) {
|
|
86
|
+
const j = closing(toks, i);
|
|
87
|
+
fromNames(toks.slice(i + 1, j), out);
|
|
88
|
+
i = j;
|
|
89
|
+
} else if (isWord(t, 'SELECT')) select = true;
|
|
90
|
+
else if (isWord(t, 'FROM') && select && !isWord(toks[i - 1], 'DISTINCT')) i = fromClause(toks, i + 1, out) - 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readColumnNames(c) {
|
|
95
|
+
return c.items().map((item) => {
|
|
96
|
+
if (item.length !== 1 || !isName(item[0])) throw new Db2Syntax(`expected an unqualified column name${item[0] ? ` at '${item[0].v}'` : ''}`, item[0] || c.peek(-1));
|
|
97
|
+
return item[0].v;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readCommonTableExpression(c) {
|
|
102
|
+
const t = c.next();
|
|
103
|
+
if (!isName(t)) { c.pos -= t ? 1 : 0; refuse(c, 'a common table expression name'); }
|
|
104
|
+
const cte = { name: t.v, columns: c.isOp('(') ? readColumnNames(c) : null, fullselect: null };
|
|
105
|
+
c.expectWord('AS');
|
|
106
|
+
if (!c.isOp('(')) refuse(c, "'(' and the common table expression's fullselect");
|
|
107
|
+
cte.fullselect = c.group();
|
|
108
|
+
return cte;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readView(c) {
|
|
112
|
+
c.expectWord('CREATE');
|
|
113
|
+
if (c.isWord('OR') && c.isWord('REPLACE', 1)) refuse(c, 'VIEW (Db2 for z/OS has no CREATE OR REPLACE VIEW)');
|
|
114
|
+
if (!c.isWord('VIEW')) refuse(c, 'VIEW');
|
|
115
|
+
c.next();
|
|
116
|
+
if (c.isWord('IF') && c.isWord(['NOT', 'EXISTS'], 1)) refuse(c, 'a view name');
|
|
117
|
+
const parts = readName(c);
|
|
118
|
+
if (parts.length > 3) refuse(c, 'a view name of at most three parts');
|
|
119
|
+
const node = { kind: 'CREATE VIEW', name: parts.join('.'), columns: null, with: [], fullselect: null, checkOption: null, tables: [] };
|
|
120
|
+
if (c.isOp('(')) node.columns = readColumnNames(c);
|
|
121
|
+
if (!c.word('AS')) refuse(c, node.columns ? 'AS' : "'(' or AS");
|
|
122
|
+
if (c.word('WITH')) {
|
|
123
|
+
if (c.isWord('RECURSIVE')) refuse(c, 'a common table expression (WITH RECURSIVE is not Db2 for z/OS syntax)');
|
|
124
|
+
do node.with.push(readCommonTableExpression(c)); while (c.op(','));
|
|
125
|
+
}
|
|
126
|
+
if (!c.isWord('SELECT') && !c.isOp('(')) refuse(c, 'a fullselect');
|
|
127
|
+
node.fullselect = c.until((t) => isWord(t, 'WITH'));
|
|
128
|
+
if (c.word('WITH')) {
|
|
129
|
+
const how = c.word(['CASCADED', 'LOCAL']);
|
|
130
|
+
if (!c.isWord('CHECK')) refuse(c, how ? 'CHECK OPTION' : 'CASCADED, LOCAL or CHECK OPTION');
|
|
131
|
+
c.next();
|
|
132
|
+
c.expectWord('OPTION');
|
|
133
|
+
node.checkOption = how ? how.u : 'CASCADED';
|
|
134
|
+
if (!c.done()) refuse(c, 'the end of the statement after CHECK OPTION');
|
|
135
|
+
}
|
|
136
|
+
const names = [];
|
|
137
|
+
for (const cte of node.with) fromNames(cte.fullselect, names);
|
|
138
|
+
fromNames(node.fullselect, names);
|
|
139
|
+
const local = new Set(node.with.map((cte) => cte.name.toUpperCase()));
|
|
140
|
+
node.tables = [...new Set(names)].filter((n) => n.includes('.') || !local.has(n.toUpperCase()));
|
|
141
|
+
return node;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const parsers = {
|
|
145
|
+
'CREATE VIEW': (c) => readView(c),
|
|
146
|
+
};
|
package/lib/diff.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { FLOW_MODEL, SCHEMA_VERSION, TOOL_VERSION } from './version.mjs';
|
|
|
9
9
|
// A diff finding is located by program rather than by line, so it orders on its own key. The text
|
|
10
10
|
// comparison underneath it is the shared one.
|
|
11
11
|
import { byText, evidenceMap } from './kernel/findings.mjs';
|
|
12
|
-
import { directoryTree, gitTree } from './kernel/source-tree.mjs';
|
|
12
|
+
import { directoryTree, gitTree, pdsExportRevision, pdsExportTree } from './kernel/source-tree.mjs';
|
|
13
13
|
import { printable } from './kernel/printable.mjs';
|
|
14
14
|
import { loadSite, SITE_FILE } from './site.mjs';
|
|
15
15
|
import { loadBaseline, BASELINE_FILE, SUPPRESSING } from './baseline.mjs';
|
|
@@ -258,6 +258,7 @@ function siteChange(baseAt, headAt) {
|
|
|
258
258
|
if (b.allowUnvalidatedPacks !== h.allowUnvalidatedPacks) parts.push(`allowUnvalidatedPacks ${h.allowUnvalidatedPacks}`);
|
|
259
259
|
if (JSON.stringify(b.runtimeVersions) !== JSON.stringify(h.runtimeVersions)) parts.push('runtimeVersions');
|
|
260
260
|
if (h.problems.length && h.problems.join() !== b.problems.join()) parts.push(`now reports: ${h.problems[0]}`);
|
|
261
|
+
if (h.warnings.length && h.warnings.join() !== b.warnings.join()) parts.push(`now warns: ${h.warnings[0]}`);
|
|
261
262
|
return parts.length ? { file: SITE_FILE, change: printable(parts.join('; '), 400) } : null;
|
|
262
263
|
}
|
|
263
264
|
|
|
@@ -307,14 +308,25 @@ export function withRefs(repo, baseRef, headRef, opts, fn) {
|
|
|
307
308
|
}
|
|
308
309
|
}
|
|
309
310
|
|
|
311
|
+
// The members of a PDS export's working tree whose files git tracks or would track: an archive's
|
|
312
|
+
// members by the archive's file.
|
|
313
|
+
function exportScope(repo, tree) {
|
|
314
|
+
const scope = gitScope(repo);
|
|
315
|
+
return new Set(tree.list().filter((p) => scope.has(resolve(repo, tree.origin(p).replace(/\([^)]*\)$/, '')))));
|
|
316
|
+
}
|
|
317
|
+
|
|
310
318
|
// Reads each revision out of git without writing it anywhere; the head is the working tree when
|
|
311
|
-
// headRef is null.
|
|
319
|
+
// headRef is null. With pdsExport each side is read as a PDS export, members named for their data
|
|
320
|
+
// sets on both.
|
|
312
321
|
export function diffRefs(repo, baseRef, headRef = null, opts = {}) {
|
|
313
322
|
const trees = { systemDirs: opts.systemDirs || [] };
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const
|
|
323
|
+
const revision = (ref) => (opts.pdsExport ? pdsExportRevision(repo, ref, trees) : gitTree(repo, ref, trees));
|
|
324
|
+
const base = revision(baseRef);
|
|
325
|
+
const head = headRef ? revision(headRef) : opts.pdsExport ? pdsExportTree(repo, trees) : repo;
|
|
326
|
+
const allow = headRef ? null : opts.pdsExport ? exportScope(repo, head) : gitScope(repo);
|
|
327
|
+
const res = diffTrees(base, head, allow ? { ...opts, allow } : opts);
|
|
317
328
|
res.summary.base = baseRef;
|
|
318
329
|
res.summary.head = headRef || WORKING_TREE;
|
|
330
|
+
if (opts.pdsExport) res.summary.pdsExport = { base: base.export, head: head.export };
|
|
319
331
|
return res;
|
|
320
332
|
}
|
package/lib/evidence/cli.mjs
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// `cobolwork evidence verify|seal|anchor`: the command half of docs/spec/evidence.md §7-9. Returns
|
|
3
3
|
// the exit status; writes one JSON document through `write`.
|
|
4
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { anchorGit, envelopeFor, seal, sealFiles, timeStampRequest } from './seal.mjs';
|
|
7
7
|
import { parseAllowedSigners } from './sshsig.mjs';
|
|
8
8
|
import { EvidenceRefusal, createExclusive, evidenceDirFrom, syncClose, writeAll } from './store.mjs';
|
|
9
9
|
import { verifyEvidence } from './verify.mjs';
|
|
10
|
+
import { EVIDENCE_RESULT_SCHEMA_VERSION } from '../version.mjs';
|
|
11
|
+
import { evidenceClausesFor } from '../compliance.mjs';
|
|
10
12
|
|
|
11
13
|
const VERBS = ['verify', 'seal', 'anchor', 'sign'];
|
|
12
14
|
|
|
15
|
+
// The clauses a verified directory's evidence answers: a run journal's records, and the chain once
|
|
16
|
+
// it verifies.
|
|
17
|
+
function complianceOf(dir, verdict) {
|
|
18
|
+
const runs = join(dir, 'runs');
|
|
19
|
+
const journals = existsSync(runs) && readdirSync(runs).some((n) => n.endsWith('.jsonl'));
|
|
20
|
+
const present = journals ? ['journal-records', ...(verdict.verified ? ['journal-chain'] : [])] : [];
|
|
21
|
+
return present.flatMap((evidenceId) => evidenceClausesFor(evidenceId).map((c) => ({ evidenceId, ...c })));
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
function anchorNewest(dir, opts) {
|
|
14
25
|
const seals = sealFiles(dir);
|
|
15
26
|
if (!seals.length) throw new EvidenceRefusal('there is no seal to anchor; run evidence seal first');
|
|
@@ -46,7 +57,7 @@ export function evidenceCommand(verb, opts, { toolVersion, write, env = process.
|
|
|
46
57
|
}
|
|
47
58
|
const dir = evidenceDirFrom(opts, env);
|
|
48
59
|
if (!dir) throw new EvidenceRefusal('evidence needs --evidence <dir> or COBOLWORK_EVIDENCE');
|
|
49
|
-
const emit = (doc) => write(`${JSON.stringify({ tool: 'cobolwork-evidence', verb, ...doc }, null, opts.quiet ? 0 : 1)}\n`);
|
|
60
|
+
const emit = (doc) => write(`${JSON.stringify({ tool: 'cobolwork-evidence', schemaVersion: EVIDENCE_RESULT_SCHEMA_VERSION, verb, ...doc }, null, opts.quiet ? 0 : 1)}\n`);
|
|
50
61
|
|
|
51
62
|
if (verb === 'verify') {
|
|
52
63
|
const allowed = opts.allowedSigners ? parseAllowedSigners(readFileSync(resolve(opts.allowedSigners), 'utf8')) : null;
|
|
@@ -56,7 +67,7 @@ export function evidenceCommand(verb, opts, { toolVersion, write, env = process.
|
|
|
56
67
|
const file = (x) => (x ? resolve(x) : null);
|
|
57
68
|
const cosign = opts.cosignBundle ? { bundle: file(opts.cosignBundle), key: file(opts.cosignKey), identity: opts.certificateIdentity || null, issuer: opts.certificateOidcIssuer || null, trustedRoot: file(opts.trustedRoot), ignoreTlog: opts.insecureIgnoreTlog === true } : null;
|
|
58
69
|
const verdict = verifyEvidence(dir, { allowed, anchorGit: file(opts.anchorGit), anchorRef: opts.ref || null, anchorPin: opts.anchorPin || null, tsr: file(opts.tsr), tsaCa: file(opts.tsaCa), cosign, maxUnsealed: max, env });
|
|
59
|
-
emit(verdict);
|
|
70
|
+
emit({ ...verdict, compliance: complianceOf(dir, verdict) });
|
|
60
71
|
return verdict.exit;
|
|
61
72
|
}
|
|
62
73
|
if (verb === 'seal') {
|
package/lib/evidence/record.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export const KINDS = {
|
|
|
33
33
|
verdict: { fields: { verdict: str, checks: any, relaxed: strings, exit: int }, required: ['verdict'] },
|
|
34
34
|
output: { fields: { name: str, sha256: hex64, bytes: int, path: str, stdout: (x) => x === true }, required: ['name', 'sha256'] },
|
|
35
35
|
// ironwork's run journal closes with the program's true RETURN-CODE, which may be negative.
|
|
36
|
-
close: { fields: { exit: (x) => x === null || Number.isSafeInteger(x), counts: any, durationMs: int, ledger: str }, required: ['exit'] },
|
|
36
|
+
close: { fields: { exit: (x) => x === null || Number.isSafeInteger(x), counts: any, durationMs: int, ledger: str, executor: (x) => ['vm', 'interpreter'].includes(x) }, required: ['exit'] },
|
|
37
37
|
// Written by ironwork's run journal, in this format, so one verifier reads both tools.
|
|
38
38
|
dd: { fields: { dd: str, event: (x) => ['open', 'close', 'end'].includes(x), mode: str, sha256: hex64, bytes: int }, required: ['dd', 'event'] },
|
|
39
39
|
call: { fields: { program: str, from: str, sha256: hex64 }, required: ['program'] },
|
package/lib/evidence/store.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// The evidence directory on disk: where it may be, how its files are opened (never through a
|
|
3
3
|
// symbolic link, owner-only), the ledger lock, and reading a chain file back line by line.
|
|
4
|
-
import {
|
|
5
|
-
import { closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, renameSync, statSync, unlinkSync, writeSync } from 'node:fs';
|
|
4
|
+
import { closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, renameSync, statSync, unlinkSync, writeSync } from 'node:fs';
|
|
6
5
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
7
6
|
|
|
8
7
|
const { O_WRONLY, O_CREAT, O_EXCL, O_APPEND, O_NOFOLLOW } = constants;
|
|
@@ -139,47 +138,61 @@ export function lastLine(path) {
|
|
|
139
138
|
const pidAlive = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } };
|
|
140
139
|
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
141
140
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
141
|
+
// A lock file as read: its inode and text, the pid it names and its age, or null when there is none.
|
|
142
|
+
// A lock whose pid and time cannot be read was left by a writer that died between creating it and
|
|
143
|
+
// writing them; it is aged from its modification time.
|
|
144
|
+
function readLock(path, now) {
|
|
145
|
+
let fd;
|
|
146
|
+
try { fd = openRead(path); } catch { return null; }
|
|
147
|
+
try {
|
|
148
|
+
const st = fstatSync(fd);
|
|
149
|
+
const text = readAll(fd).toString('utf8');
|
|
150
|
+
const [p, t] = text.trim().split(/\s+/).map(Number);
|
|
151
|
+
const pid = Number.isSafeInteger(p) && p > 0 ? p : null;
|
|
152
|
+
const since = Number.isSafeInteger(t) && t > 0 ? t : null;
|
|
153
|
+
return { ino: st.ino, text, pid, age: pid !== null && since !== null ? now() - since : now() - st.mtimeMs };
|
|
154
|
+
} finally { closeSync(fd); }
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
//
|
|
157
|
+
const isStale = (lock, staleMs) => lock !== null && lock.age > staleMs && (lock.pid === null || !pidAlive(lock.pid));
|
|
158
|
+
|
|
159
|
+
// Replaces the stale lock `seen` with this writer's own. Breakers hold ledger.lock.break while they
|
|
160
|
+
// break, so one breaks at a time, and rename it over the lock only while the lock is still the file
|
|
161
|
+
// judged stale: a lock a peer took since is never moved. Returns whether this writer holds the lock.
|
|
162
|
+
export function breakStale(path, seen, { staleMs = 60000, now = Date.now } = {}) {
|
|
163
|
+
const claim = `${path}.break`;
|
|
164
|
+
let fd;
|
|
165
|
+
try { fd = createExclusive(claim); } catch (e) {
|
|
166
|
+
if (e.code !== 'EEXIST') throw e;
|
|
167
|
+
if (isStale(readLock(claim, now), staleMs)) { try { unlinkSync(claim); } catch { /* already gone */ } }
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
writeAll(fd, `${process.pid} ${now()}\n`);
|
|
171
|
+
closeSync(fd);
|
|
172
|
+
const still = readLock(path, now);
|
|
173
|
+
if (still && still.ino === seen.ino && still.text === seen.text) {
|
|
174
|
+
try { renameSync(claim, path); return true; } catch { /* the claim is removed below */ }
|
|
175
|
+
}
|
|
176
|
+
try { unlinkSync(claim); } catch { /* already gone */ }
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Takes ledger.lock, breaking one that is older than staleMs and whose holder is gone. Returns
|
|
181
|
+
// { release, broken } or null when the lock could not be had within waitMs.
|
|
158
182
|
export function takeLock(path, { waitMs = 5000, staleMs = 60000, now = Date.now } = {}) {
|
|
159
183
|
const deadline = now() + waitMs;
|
|
160
|
-
|
|
184
|
+
const held = (broken) => ({ release: () => { try { unlinkSync(path); } catch { /* already gone */ } }, broken });
|
|
161
185
|
for (;;) {
|
|
162
186
|
try {
|
|
163
187
|
const fd = createExclusive(path);
|
|
164
188
|
writeAll(fd, `${process.pid} ${now()}\n`);
|
|
165
189
|
closeSync(fd);
|
|
166
|
-
return
|
|
190
|
+
return held(null);
|
|
167
191
|
} catch (e) {
|
|
168
192
|
if (e.code !== 'EEXIST') throw e;
|
|
169
193
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
try { st = statSync(path); text = readFileSync(path, 'utf8'); } catch { /* removed between the open and the read */ }
|
|
173
|
-
if (text !== null) {
|
|
174
|
-
const [p, t] = text.trim().split(/\s+/).map(Number);
|
|
175
|
-
const pid = Number.isSafeInteger(p) && p > 0 ? p : null;
|
|
176
|
-
const since = Number.isSafeInteger(t) && t > 0 ? t : null;
|
|
177
|
-
const age = pid !== null && since !== null ? now() - since : now() - st.mtimeMs;
|
|
178
|
-
if (age > staleMs && (pid === null || !pidAlive(pid)) && breakStale(path, { text, ino: st.ino })) {
|
|
179
|
-
broken = { holderPid: pid, ageMs: Math.max(0, Math.trunc(age)) };
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
194
|
+
const seen = readLock(path, now);
|
|
195
|
+
if (isStale(seen, staleMs) && breakStale(path, seen, { staleMs, now })) return held({ holderPid: seen.pid, ageMs: Math.max(0, Math.trunc(seen.age)) });
|
|
183
196
|
if (now() >= deadline) return null;
|
|
184
197
|
sleep(25);
|
|
185
198
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// One reading of an EXEC block for every rule set and the flow engine, and the one the precompiler's
|
|
3
|
+
// table names: each option's argument tokens, every word in order, and for EXEC CICS the command the
|
|
4
|
+
// table names it and the direction each option's data moves. Cached per block, so a worker that
|
|
5
|
+
// rebuilds a program's parse reads each block exactly as the main thread does.
|
|
6
|
+
import { cicsCommand } from './precompile-cics.mjs';
|
|
7
|
+
import { CICS_COMMANDS, CICS_EVERY_COMMAND } from './cics-commands.mjs';
|
|
8
|
+
|
|
9
|
+
const readings = new WeakMap();
|
|
10
|
+
|
|
11
|
+
// `opts` maps each option to the tokens inside its parentheses, the first time it is written; `words`
|
|
12
|
+
// is every word of the block. `verb` and `sub` are the command's first two words: the table's name
|
|
13
|
+
// where it has one (SEND MAP however its options are ordered), else the block's first two words.
|
|
14
|
+
export function execReading(exec) {
|
|
15
|
+
let r = readings.get(exec);
|
|
16
|
+
if (r) return r;
|
|
17
|
+
const toks = exec.toks;
|
|
18
|
+
const opts = new Map();
|
|
19
|
+
const words = [];
|
|
20
|
+
const top = [];
|
|
21
|
+
let depth = 0;
|
|
22
|
+
for (let i = 0; i < toks.length; i++) {
|
|
23
|
+
const t = toks[i];
|
|
24
|
+
if (t.t === 'sep') { depth += t.v === '(' ? 1 : t.v === ')' ? -1 : 0; continue; }
|
|
25
|
+
if (t.t !== 'word') continue;
|
|
26
|
+
words.push(t.u);
|
|
27
|
+
if (depth === 0) top.push({ word: t.u });
|
|
28
|
+
if (toks[i + 1] && toks[i + 1].t === 'sep' && toks[i + 1].v === '(') {
|
|
29
|
+
const inner = [];
|
|
30
|
+
let d = 1;
|
|
31
|
+
for (let k = i + 2; k < toks.length && d > 0; k++) {
|
|
32
|
+
if (toks[k].t === 'sep') { d += toks[k].v === '(' ? 1 : -1; if (d === 0) break; continue; }
|
|
33
|
+
inner.push(toks[k]);
|
|
34
|
+
}
|
|
35
|
+
if (!opts.has(t.u)) opts.set(t.u, inner);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const command = exec.kind === 'CICS' && top.length ? cicsCommand(top) : null;
|
|
39
|
+
const named = command ? command.split(' ') : [];
|
|
40
|
+
const entry = command ? CICS_COMMANDS[command] : null;
|
|
41
|
+
r = {
|
|
42
|
+
opts, words, command,
|
|
43
|
+
verb: named[0] || words[0],
|
|
44
|
+
sub: named.length > 1 ? named[1] : words[1],
|
|
45
|
+
// The way an option's data moves, as the table records it: sends, receives, both, label or none;
|
|
46
|
+
// null for an option or a command the table does not hold.
|
|
47
|
+
direction: (option) => (entry ? entry.options[option] || CICS_EVERY_COMMAND[option] || entry.anyOption || null : null),
|
|
48
|
+
};
|
|
49
|
+
readings.set(exec, r);
|
|
50
|
+
return r;
|
|
51
|
+
}
|
package/lib/execution.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Execution: whether the estate's own runs entered the paragraph a finding is in, from the
|
|
3
3
|
// reports `ironwork run --coverage` writes. A finding in a paragraph a run entered is code the tests
|
|
4
|
-
// exercise; one in a paragraph no run entered is code they never reach. See docs/spec/evidence.md §13.
|
|
4
|
+
// exercise; one in a paragraph no run entered is code they never reach. See docs/spec/evidence.md §13.5 (execution coverage).
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
6
|
import { basename, delimiter, join } from 'node:path';
|
|
7
7
|
|
|
@@ -11,7 +11,8 @@ export function executionFeedPaths(opts = {}) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// One report: per PROGRAM-ID, each paragraph's source line, name and how often a run entered it.
|
|
14
|
-
// Shape (ironwork's): { programs: [{ program, detail: [{ name, line, entered }] }] }
|
|
14
|
+
// Shape (ironwork's): { programs: [{ program, detail: [{ name, line, entered }] }] }, as
|
|
15
|
+
// schema/cobolwork-execution.schema.json describes it.
|
|
15
16
|
export function loadExecutionFeed(path) {
|
|
16
17
|
const feed = { file: basename(path), programs: new Map(), problem: null };
|
|
17
18
|
let doc;
|
package/lib/explain.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { REGISTRY } from './kernel/registry.mjs';
|
|
|
8
8
|
import { WHO_ACTS } from './tui/model.mjs';
|
|
9
9
|
import { verificationPlan } from './verify.mjs';
|
|
10
10
|
import { cobolCard, statementCard } from './cards.mjs';
|
|
11
|
+
import { EXPLAIN_SCHEMA_VERSION } from './version.mjs';
|
|
11
12
|
|
|
12
13
|
const HIDDEN_RULES = new Set(Object.keys(REGISTRY.find((s) => s.name === 'hidden').rules));
|
|
13
14
|
|
|
@@ -151,6 +152,7 @@ export function explainFinding(report, fingerprint, { root }) {
|
|
|
151
152
|
|
|
152
153
|
return {
|
|
153
154
|
tool: 'cobolwork-explain',
|
|
155
|
+
schemaVersion: EXPLAIN_SCHEMA_VERSION,
|
|
154
156
|
carriesSource: true,
|
|
155
157
|
note: NOTE,
|
|
156
158
|
fingerprint,
|
package/lib/exploitability.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { kindsOf } from './consequence.mjs';
|
|
|
13
13
|
import { WHO } from './sets/flow.mjs';
|
|
14
14
|
import { entryName, accessOfEntry, accessFromEntry, privilegedFromEntry } from './reach.mjs';
|
|
15
15
|
import { verifyEvidence } from './evidence/verify.mjs';
|
|
16
|
+
import { versionProblem, unknownKeys, ignoredKey } from './kernel/version-key.mjs';
|
|
16
17
|
|
|
17
18
|
export const HANDLING = 'This report joins routes an attacker can drive with the estate\'s own access facts. It holds no source '
|
|
18
19
|
+ 'text, but it is a list of what to attack first: keep it where the access records it was built from may go, and do not '
|
|
@@ -20,7 +21,7 @@ export const HANDLING = 'This report joins routes an attacker can drive with the
|
|
|
20
21
|
|
|
21
22
|
// A system response is not input, but the caller who provokes the failure is the one who reads it back.
|
|
22
23
|
const DRIVER = { ...WHO, 'system-response': 'A web caller, who can provoke the failure and reads the code sent back' };
|
|
23
|
-
const AT_REST = { 'file-record': 'the file record', database: 'the database row' };
|
|
24
|
+
const AT_REST = { 'file-record': 'the file record', 'cics-queue': 'the queue item', database: 'the database row' };
|
|
24
25
|
|
|
25
26
|
const at = (x) => `${x.file}:${x.line}`;
|
|
26
27
|
|
|
@@ -167,6 +168,9 @@ export function witnessFeedPaths(opts = {}) {
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
const OUTCOMES = new Set(['reproduced', 'not-reproduced']);
|
|
171
|
+
export const WITNESS_VERSION = 1;
|
|
172
|
+
const WITNESS_KEYS = new Set(['$schema', 'version', 'witness', 'recorded', 'results']);
|
|
173
|
+
const RESULT_KEYS = new Set(['outcome', 'by', 'on', 'system', 'reference', 'evidence']);
|
|
170
174
|
const str = (v) => typeof v === 'string' && v.length > 0;
|
|
171
175
|
const isoDate = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v);
|
|
172
176
|
|
|
@@ -198,7 +202,7 @@ function witnessedRun(ev, base, verdicts) {
|
|
|
198
202
|
// { witness, recorded, results: { FINGERPRINT: { outcome: "reproduced"|"not-reproduced", by, on, system, reference? } } }
|
|
199
203
|
// Only those fields are read. What was sent to the system is not one of them.
|
|
200
204
|
export function loadWitnessFeed(path, { root = null } = {}) {
|
|
201
|
-
const feed = { file: basename(path), witness: null, recorded: null, results: new Map(), refused: [], problem: null };
|
|
205
|
+
const feed = { file: basename(path), witness: null, recorded: null, results: new Map(), refused: [], warnings: [], problem: null };
|
|
202
206
|
let real;
|
|
203
207
|
try { real = realpathSync(path); } catch (e) { feed.problem = `could not be read (${e.code || e.name})`; return feed; }
|
|
204
208
|
if (root) {
|
|
@@ -207,11 +211,16 @@ export function loadWitnessFeed(path, { root = null } = {}) {
|
|
|
207
211
|
}
|
|
208
212
|
let doc;
|
|
209
213
|
try { doc = JSON.parse(readFileSync(real, 'utf8')); } catch (e) { feed.problem = `is not JSON (${e.message})`; return feed; }
|
|
214
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) { feed.problem = 'is not a JSON object'; return feed; }
|
|
215
|
+
feed.problem = versionProblem(doc, WITNESS_VERSION);
|
|
216
|
+
if (feed.problem) return feed;
|
|
210
217
|
if (!str(doc.witness)) { feed.problem = 'does not say whose tests it holds'; return feed; }
|
|
211
218
|
if (!isoDate(doc.recorded)) { feed.problem = 'does not say when it was recorded'; return feed; }
|
|
212
219
|
if (!doc.results || typeof doc.results !== 'object' || Array.isArray(doc.results)) { feed.problem = 'holds no results'; return feed; }
|
|
213
220
|
feed.witness = doc.witness;
|
|
214
221
|
feed.recorded = doc.recorded;
|
|
222
|
+
const extra = new Set(Object.values(doc.results).flatMap((r) => (r && typeof r === 'object' ? unknownKeys(r, RESULT_KEYS) : [])));
|
|
223
|
+
feed.warnings = [...unknownKeys(doc, WITNESS_KEYS).map((k) => ignoredKey(feed.file, k)), ...[...extra].sort().map((k) => ignoredKey(`${feed.file} results`, k))];
|
|
215
224
|
const verdicts = new Map();
|
|
216
225
|
for (const [fingerprint, r] of Object.entries(doc.results)) {
|
|
217
226
|
const why = !r || typeof r !== 'object' ? 'is not an object'
|
package/lib/hlasm/asm/data.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// type and type extension, program type, length, scale and exponent modifiers, and nominal value.
|
|
4
4
|
// https://www.ibm.com/docs/en/hla-and-tf/1.6.0?topic=statements-dc-instruction
|
|
5
5
|
import { HlasmSyntax, splitOperands } from '../operands.mjs';
|
|
6
|
-
import { readExpression } from '../expr.mjs';
|
|
6
|
+
import { parseExpression, readExpression } from '../expr.mjs';
|
|
7
7
|
|
|
8
8
|
const EXTENSIONS = {
|
|
9
9
|
C: 'AEU', G: '', X: '', B: '', F: 'D', H: '', E: 'HBD', D: 'HBD', L: 'HBDQ', P: '', Z: '',
|
|
@@ -110,7 +110,19 @@ function operands(st, nominalRequired) {
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// A channel command word: command code, data address, flags and data count, each an expression.
|
|
114
|
+
// https://www.ibm.com/docs/en/hla-and-tf/1.6.0?topic=statements-ccw-ccw0-instructions
|
|
115
|
+
function ccw(st) {
|
|
116
|
+
const list = splitOperands(st.field || '');
|
|
117
|
+
if (list.length !== 4) throw new HlasmSyntax(`${st.operation} takes a command code, data address, flags and data count, not ${list.length} operands`);
|
|
118
|
+
const [command, address, flags, count] = list.map((text) => parseExpression(text.trim()));
|
|
119
|
+
return { kind: st.operation, command, address, flags, count };
|
|
120
|
+
}
|
|
121
|
+
|
|
113
122
|
export const parsers = {
|
|
114
123
|
DC: (st) => ({ kind: 'DC', operands: operands(st, true) }),
|
|
115
124
|
DS: (st) => ({ kind: 'DS', operands: operands(st, false) }),
|
|
125
|
+
CCW: ccw,
|
|
126
|
+
CCW0: ccw,
|
|
127
|
+
CCW1: ccw,
|
|
116
128
|
};
|
|
@@ -129,7 +129,9 @@ function dxd(st) {
|
|
|
129
129
|
|
|
130
130
|
export const parsers = {
|
|
131
131
|
CSECT: (st) => noOperand('CSECT', st, false),
|
|
132
|
-
|
|
132
|
+
// An unnamed dummy section, like an unnamed COM, is defined when the name entry is omitted.
|
|
133
|
+
// https://www.ibm.com/docs/en/hla-and-tf/1.6.0?topic=sections-unnamed-section
|
|
134
|
+
DSECT: (st) => noOperand('DSECT', st, false),
|
|
133
135
|
RSECT: (st) => noOperand('RSECT', st, false),
|
|
134
136
|
COM: (st) => noOperand('COM', st, false),
|
|
135
137
|
START: (st) => {
|
package/lib/hlasm/instr.mjs
CHANGED
|
@@ -27,6 +27,26 @@ function shapeOf(pattern) {
|
|
|
27
27
|
|
|
28
28
|
export const formatFamily = (format) => String(format || '').replace(/-[a-z]$/, '');
|
|
29
29
|
|
|
30
|
+
// An explicit length is assembled into a length field: 8 bits for L, 4 bits for L1 and L2, each
|
|
31
|
+
// holding the length less one, so 0 to 256 and 0 to 16 bytes. Checked when the length is a
|
|
32
|
+
// constant; a symbol's value needs the symbol table.
|
|
33
|
+
const LENGTH_FIELD = { L: 256, L1: 16, L2: 16 };
|
|
34
|
+
function constant(e) {
|
|
35
|
+
if (!e) return null;
|
|
36
|
+
if (e.t === 'num') return e.v;
|
|
37
|
+
if (e.t === 'sdt' && e.type === 'X') return /^[0-9A-F]+$/i.test(e.text) ? parseInt(e.text, 16) : null;
|
|
38
|
+
if (e.t === 'sdt' && e.type === 'B') return /^[01]+$/.test(e.text) ? parseInt(e.text, 2) : null;
|
|
39
|
+
if (e.t === 'paren') return constant(e.e);
|
|
40
|
+
if (e.t === 'unary') { const v = constant(e.e); return v === null ? null : e.op === '-' ? -v : v; }
|
|
41
|
+
if (e.t === 'bin') {
|
|
42
|
+
const l = constant(e.l);
|
|
43
|
+
const r = constant(e.r);
|
|
44
|
+
if (l === null || r === null) return null;
|
|
45
|
+
return { '+': l + r, '-': l - r, '*': l * r, '/': r ? Math.trunc(l / r) : 0 }[e.op] ?? null;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
export function parseInstruction(st, row) {
|
|
31
51
|
const shape = shapeOf(row.operands || '');
|
|
32
52
|
// An instruction with no operands takes whatever follows it as remarks: SAM24 Back to AMODE 24.
|
|
@@ -42,6 +62,11 @@ export function parseInstruction(st, row) {
|
|
|
42
62
|
if (!s.parts) return { operand: s.name, e: parseExpression(text) };
|
|
43
63
|
const a = parseAddress(text);
|
|
44
64
|
if (a.parts && a.parts.length > s.parts) throw new HlasmSyntax(`operand ${i + 1} of ${st.operation} (${s.name}) has ${a.parts.length} parts in parentheses, not at most ${s.parts}`);
|
|
65
|
+
const field = /^D\d*\((L\d?),/.exec(s.name)?.[1];
|
|
66
|
+
const length = field && a.parts ? constant(a.parts[0]) : null;
|
|
67
|
+
if (length !== null && (length < 0 || length > LENGTH_FIELD[field])) {
|
|
68
|
+
throw new HlasmSyntax(`operand ${i + 1} of ${st.operation} has length ${length}, and ${field} holds 0 to ${LENGTH_FIELD[field]}`);
|
|
69
|
+
}
|
|
45
70
|
return { operand: s.name, ...a };
|
|
46
71
|
});
|
|
47
72
|
return { kind: 'INSTRUCTION', mnemonic: st.operation, format: row.format, length: row.length, operands };
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Parsers for the z/OS authorization macros MODESET, TESTAUTH, and RACROUTE.
|
|
3
|
-
import { HlasmSyntax, NAME } from '../operands.mjs';
|
|
3
|
+
import { HlasmSyntax, NAME, remoteForm } from '../operands.mjs';
|
|
4
|
+
import { MVS38_EXTKEYS, mvs38Form } from '../mvs38.mjs';
|
|
4
5
|
|
|
5
6
|
const MODESET_EXTKEYS = new Set(['ZERO', 'TCB', 'RBT1', 'RBT234', 'KEY2', 'KEY3', 'KEY4', 'KEY7']);
|
|
6
7
|
const MODESET_KEYS = new Set(['ZERO', 'NZERO']);
|
|
7
8
|
const MODESET_MODES = new Set(['PROB', 'SUP']);
|
|
8
|
-
const MODESET_KNOWN = new Set(['EXTKEY', 'KEYADDR', 'KEYREG', 'SAVEKEY', 'WORKREG', 'RELATED', 'KEY', 'MODE']);
|
|
9
|
+
const MODESET_KNOWN = new Set(['EXTKEY', 'KEYADDR', 'KEYREG', 'SAVEKEY', 'WORKREG', 'RELATED', 'KEY', 'MODE', 'MF']);
|
|
10
|
+
// The list and execute forms of the MODESET that generates an SVC (z/OS 3.1 MVS Programming:
|
|
11
|
+
// Authorized Assembler Services Reference, Volume 3): the list form holds KEY and MODE, the execute
|
|
12
|
+
// form names the list.
|
|
13
|
+
const MODESET_LIST_KNOWN = new Set(['KEY', 'MODE', 'RELATED', 'MF']);
|
|
14
|
+
const MODESET_EXECUTE_KNOWN = new Set(['RELATED', 'MF']);
|
|
9
15
|
|
|
10
16
|
const TESTAUTH_KNOWN = new Set(['FCTN', 'STATE', 'KEY', 'RBLEVEL', 'BRANCH']);
|
|
11
17
|
const TESTAUTH_STATES = new Set(['YES', 'NO']);
|
|
12
18
|
|
|
13
19
|
const RACROUTE_REQUESTS = new Set(['AUTH', 'FASTAUTH', 'DEFINE', 'DIRAUTH', 'EXTRACT', 'LIST', 'SIGNON', 'STAT', 'TOKENBLD', 'TOKENMAP', 'TOKENXTR', 'VERIFY', 'VERIFYX', 'AUDIT']);
|
|
14
20
|
|
|
15
|
-
function parseModeset(st, ops) {
|
|
21
|
+
function parseModeset(st, ops, opts) {
|
|
16
22
|
const { keywords } = ops;
|
|
17
23
|
const node = { kind: 'MODESET', key: null, mode: null, extkey: null, keywords: {} };
|
|
18
24
|
|
|
@@ -23,9 +29,23 @@ function parseModeset(st, ops) {
|
|
|
23
29
|
node.keywords[k] = v;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
if (keywords.has('MF')) {
|
|
33
|
+
const { form, list } = remoteForm(keywords.get('MF'), 'MF');
|
|
34
|
+
const known = form === 'L' ? MODESET_LIST_KNOWN : MODESET_EXECUTE_KNOWN;
|
|
35
|
+
for (const k of keywords.keys()) {
|
|
36
|
+
if (!known.has(k)) throw new HlasmSyntax(`MODESET: the ${form === 'L' ? 'list' : 'execute'} form does not take ${k}`);
|
|
37
|
+
}
|
|
38
|
+
if (form === 'L' && !keywords.has('KEY') && !keywords.has('MODE')) throw new HlasmSyntax('MODESET: the list form needs KEY or MODE');
|
|
39
|
+
node.mf = form;
|
|
40
|
+
if (list) node.list = list;
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
if (keywords.has('EXTKEY')) {
|
|
27
44
|
const v = keywords.get('EXTKEY').trim().toUpperCase();
|
|
28
|
-
if (
|
|
45
|
+
if (MVS38_EXTKEYS.has(v)) {
|
|
46
|
+
mvs38Form(opts, node, 'MODESET', `EXTKEY=${v}`);
|
|
47
|
+
node.pswKey = MVS38_EXTKEYS.get(v);
|
|
48
|
+
} else if (!MODESET_EXTKEYS.has(v)) {
|
|
29
49
|
throw new HlasmSyntax(`MODESET: invalid EXTKEY value ${v}`);
|
|
30
50
|
}
|
|
31
51
|
node.extkey = v;
|