@portll/cobolwork 0.5.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 +89 -26
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +56 -14
- package/lib/baseline.mjs +9 -0
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +133 -53
- package/lib/capabilities.mjs +13 -11
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +17 -0
- package/lib/control-reuse.mjs +113 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +77 -4
- package/lib/dataflow.mjs +402 -152
- 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/equivalence.mjs +44 -5
- package/lib/evidence/cli.mjs +26 -7
- package/lib/evidence/record.mjs +16 -6
- package/lib/evidence/seal.mjs +17 -0
- package/lib/evidence/store.mjs +44 -15
- package/lib/evidence/timestamp.mjs +83 -0
- package/lib/evidence/verify.mjs +120 -42
- 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 +128 -0
- package/lib/hlasm/asm/listing.mjs +43 -0
- package/lib/hlasm/asm/output.mjs +45 -0
- package/lib/hlasm/asm/sections.mjs +161 -0
- package/lib/hlasm/asm/symbols.mjs +78 -0
- package/lib/hlasm/exec.mjs +27 -0
- package/lib/hlasm/expr.mjs +167 -0
- package/lib/hlasm/instr.mjs +73 -0
- package/lib/hlasm/locate.mjs +333 -0
- package/lib/hlasm/macro/authorization.mjs +146 -0
- package/lib/hlasm/macro/datasets.mjs +107 -0
- package/lib/hlasm/macro/io.mjs +162 -0
- package/lib/hlasm/macro/le.mjs +53 -0
- package/lib/hlasm/macro/linkage.mjs +143 -0
- package/lib/hlasm/macro/operator.mjs +77 -0
- package/lib/hlasm/macro/program.mjs +184 -0
- package/lib/hlasm/macro/recovery.mjs +85 -0
- package/lib/hlasm/macro/storage.mjs +167 -0
- package/lib/hlasm/macro/structured.mjs +131 -0
- package/lib/hlasm/model.mjs +97 -0
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +59 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +130 -0
- 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/inventory.mjs +10 -7
- package/lib/ironwork-ids.mjs +24 -0
- package/lib/ironwork.mjs +20 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +32 -23
- package/lib/kernel/shared-pass.mjs +163 -0
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- package/lib/options.mjs +26 -4
- 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/policy.mjs +6 -0
- package/lib/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +8 -4
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +44 -3
- package/lib/scan.mjs +28 -12
- package/lib/sets/abend.mjs +82 -8
- package/lib/sets/build.mjs +43 -37
- package/lib/sets/cics.mjs +20 -38
- package/lib/sets/compile.mjs +46 -18
- package/lib/sets/copybook.mjs +5 -3
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +32 -8
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +124 -8
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +11 -9
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/priv.mjs +5 -3
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +112 -0
- package/lib/sets/semantics.mjs +8 -3
- package/lib/sets/web.mjs +39 -23
- 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 +520 -5
- package/rules/compliance-dora.json +509 -5
- package/rules/compliance-ffiec.json +505 -1
- package/rules/compliance-nist80053.json +557 -1
- package/rules/gitleaks-mainframe.toml +46 -15
- package/rules/hlasm-instructions.json +2396 -0
- 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.policy.schema.json +7 -0
- package/schema/cobolwork.site.schema.json +116 -0
package/lib/evidence/verify.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Verifies an evidence directory and reports verified, broken, unrecorded, open, anchored, sealed,
|
|
3
3
|
// unsealedTail and signatures as separate facts. docs/spec/evidence.md §9.
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { createHash } from 'node:crypto';
|
|
5
6
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
import { JOURNAL_KINDS, LEDGER_KINDS, checkLine } from './record.mjs';
|
|
8
9
|
import { readLines } from './store.mjs';
|
|
9
10
|
import { LEDGER } from './journal.mjs';
|
|
10
|
-
import { NAMESPACE, PAYLOAD_TYPE, SEAL_PREDICATE, STATEMENT_TYPE, listWitnessed, pae, readWitnessed, resolveRef, sealFiles } from './seal.mjs';
|
|
11
|
+
import { NAMESPACE, PAYLOAD_TYPE, SEAL_PREDICATE, STATEMENT_TYPE, isAncestor, listWitnessed, pae, readWitnessed, resolveRef, sealFiles, witnessedBlobs } from './seal.mjs';
|
|
11
12
|
import { verifySshsig } from './sshsig.mjs';
|
|
13
|
+
import { readRequest, readResponse } from './timestamp.mjs';
|
|
12
14
|
|
|
13
15
|
const sha256 = (b) => createHash('sha256').update(b).digest('hex');
|
|
14
16
|
|
|
@@ -68,7 +70,108 @@ function signaturesOf(seal, allowed) {
|
|
|
68
70
|
});
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
// With a pinned commit, the witness's history since then: the pin must still be an ancestor of the
|
|
74
|
+
// ref, and every seal it held must be at the ref unchanged. A rewritten branch or a later commit
|
|
75
|
+
// that deletes seals is a contradiction, which a fresh reading of the ref alone cannot see.
|
|
76
|
+
function pinProblem(repo, pin, ref, env) {
|
|
77
|
+
const pinned = resolveRef(repo, pin, env);
|
|
78
|
+
if (!pinned) return `the pinned commit ${pin} is not in the witness`;
|
|
79
|
+
const at = resolveRef(repo, ref, env);
|
|
80
|
+
if (!isAncestor(repo, pinned, at, env)) return `the witness was rewritten: the pinned commit ${pinned.slice(0, 12)} is not in the history of ${ref}`;
|
|
81
|
+
const now = witnessedBlobs(repo, at, env);
|
|
82
|
+
for (const [path, blob] of witnessedBlobs(repo, pinned, env)) {
|
|
83
|
+
if (now.get(path) !== blob) return `the witness held ${path} at the pinned commit, and ${now.has(path) ? 'holds another' : 'no longer holds it'} at ${ref}`;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A seal a witness vouches for counts when the ledger holds the state it names and an allowed
|
|
89
|
+
// signer signed it.
|
|
90
|
+
function accepting(recs, allowed) {
|
|
91
|
+
return (ws, name) => {
|
|
92
|
+
const a = agrees(ws, recs);
|
|
93
|
+
if (!a.ok) return { sealed: false, reason: `${name}: ${a.why}` };
|
|
94
|
+
const sigs = signaturesOf(ws, allowed);
|
|
95
|
+
if (sigs.some((x) => x.valid === true)) return { sealed: true, seal: ws };
|
|
96
|
+
if (sigs.every((x) => x.valid === false)) return { sealed: false, unsigned: true, reason: `${name} carries no signature from an allowed signer` };
|
|
97
|
+
return { sealed: null, reason: allowed ? `${name} carries no valid signature` : 'no allowed signers were given' };
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const firstLine = (text) => String(text || '').trim().split('\n')[0].slice(0, 300);
|
|
102
|
+
const BOUNDED = { timeout: 30000, maxBuffer: 1 << 20, windowsHide: true };
|
|
103
|
+
|
|
104
|
+
// The git witness is the authority for its ledger: every seal it holds is checked, whether or not
|
|
105
|
+
// the directory still has its copy, so deleting local seals cannot hide a cut tail.
|
|
106
|
+
function gitWitness({ repo, ref, pin, recs, local, accept, allowed, verdict, env }) {
|
|
107
|
+
const witnessCommit = resolveRef(repo, ref, env);
|
|
108
|
+
if (!witnessCommit) return { sealed: null, reason: `${ref} does not resolve in ${repo}` };
|
|
109
|
+
verdict.witnessCommit = witnessCommit;
|
|
110
|
+
const rewritten = pin ? pinProblem(repo, pin, ref, env) : null;
|
|
111
|
+
if (rewritten) return { sealed: false, reason: rewritten };
|
|
112
|
+
const held = listWitnessed(repo, ref, env);
|
|
113
|
+
const chain = recs.length ? recs[0].chain : null;
|
|
114
|
+
const seqs = chain ? held.get(chain) || [] : [];
|
|
115
|
+
const localSeqs = new Set(local.filter((s) => s.chain === chain).map((s) => s.seq));
|
|
116
|
+
let result = null;
|
|
117
|
+
let newest = null;
|
|
118
|
+
if (!seqs.length && held.size) result = { sealed: false, reason: `the witness holds seals for ledger ${[...held.keys()].join(', ')}, and this directory holds ledger ${chain || 'none'}` };
|
|
119
|
+
else if (!seqs.length) result = { sealed: null, reason: `not on ${ref}: the witness holds no seal for ledger ${chain || 'none'}` };
|
|
120
|
+
for (const seq of result ? [] : seqs) {
|
|
121
|
+
const name = `${seq}.dsse.json`;
|
|
122
|
+
let ws;
|
|
123
|
+
try { ws = decodeSeal(readWitnessed(repo, ref, `${chain}/${name}`, env).text); } catch (e) { result = { sealed: false, reason: `the witnessed ${name} is not a seal: ${e.message}` }; break; }
|
|
124
|
+
const r = accept(ws, `witnessed ${name}`);
|
|
125
|
+
if (r.sealed === false && !r.unsigned) { result = r; break; }
|
|
126
|
+
verdict.seals.push({ seal: name, witnessed: true, local: localSeqs.has(seq), length: ws.length, signed: r.sealed === true });
|
|
127
|
+
if (r.sealed === false) { result = r; break; }
|
|
128
|
+
if (r.sealed === true) newest = ws;
|
|
129
|
+
}
|
|
130
|
+
for (const s of local) if (s.chain === chain && !seqs.includes(s.seq)) verdict.seals.push({ seal: s.name, witnessed: false, reason: `not on ${ref}` });
|
|
131
|
+
if (result) return result;
|
|
132
|
+
if (newest) return { sealed: true, seal: newest };
|
|
133
|
+
return { sealed: null, reason: allowed ? 'no witnessed seal carries a valid signature' : 'no allowed signers were given' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// An RFC 3161 response: its imprint names a seal, its nonce is the kept request's, and OpenSSL
|
|
137
|
+
// verifies the authority's signature against --tsa-ca.
|
|
138
|
+
function tsrWitness({ dir, tsr, ca, local, accept, verdict, env }) {
|
|
139
|
+
let resp;
|
|
140
|
+
try { resp = readResponse(readFileSync(tsr)); } catch (e) { return { sealed: null, reason: `--tsr ${tsr} is not a time-stamp response (${e.message})` }; }
|
|
141
|
+
if (resp.status > 1) return { sealed: null, reason: `the time-stamp authority refused the request (status ${resp.status})` };
|
|
142
|
+
const s = local.find((x) => sha256(x.text) === resp.digest);
|
|
143
|
+
if (!s) return { sealed: false, reason: `the time-stamp response's message imprint ${resp.digest.slice(0, 12)} matches no seal in seals/` };
|
|
144
|
+
verdict.timeStamp = { seal: s.name, genTime: resp.genTime };
|
|
145
|
+
const kept = join(dir, 'seals', `${s.seq}.tsq`);
|
|
146
|
+
if (!existsSync(kept)) return { sealed: null, reason: `seals/${s.seq}.tsq, the request for ${s.name}, is not kept, so the response's nonce cannot be checked` };
|
|
147
|
+
let req;
|
|
148
|
+
try { req = readRequest(readFileSync(kept)); } catch (e) { return { sealed: null, reason: `seals/${s.seq}.tsq is not a time-stamp request (${e.message})` }; }
|
|
149
|
+
if (req.digest !== resp.digest || !resp.nonce || req.nonce !== resp.nonce) return { sealed: false, reason: `the time-stamp response's nonce is not that of the request kept for ${s.name}` };
|
|
150
|
+
if (!ca) return { sealed: null, reason: '--tsr needs --tsa-ca, the certificate the time-stamp authority chains to' };
|
|
151
|
+
const r = spawnSync('openssl', ['ts', '-verify', '-in', tsr, '-data', s.path, '-CAfile', ca], { ...BOUNDED, env });
|
|
152
|
+
if (r.error) return { sealed: null, reason: `OpenSSL could not be run (${r.error.code || r.error.message}), so the authority's signature is unchecked` };
|
|
153
|
+
if (r.status !== 0) return { sealed: false, reason: `OpenSSL does not verify the time-stamp response: ${firstLine(r.stderr) || firstLine(r.stdout)}` };
|
|
154
|
+
return accept(s, `the time-stamped ${s.name}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// A transparency-log bundle, checked by cosign against each local seal, newest first.
|
|
158
|
+
function cosignWitness({ bundle, key, identity, issuer, trustedRoot, ignoreTlog, local, accept, verdict, env }) {
|
|
159
|
+
if (!key && !(identity && issuer)) return { sealed: null, reason: '--cosign-bundle needs --cosign-key, or --certificate-identity with --certificate-oidc-issuer' };
|
|
160
|
+
let last = '';
|
|
161
|
+
for (const s of [...local].reverse()) {
|
|
162
|
+
const who = key ? ['--key', key] : ['--certificate-identity', identity, '--certificate-oidc-issuer', issuer];
|
|
163
|
+
const r = spawnSync('cosign', ['verify-blob', '--bundle', bundle, ...who, ...(trustedRoot ? ['--trusted-root', trustedRoot] : []), ...(ignoreTlog ? ['--insecure-ignore-tlog=true'] : []), s.path], { ...BOUNDED, env });
|
|
164
|
+
if (r.error) return { sealed: null, reason: `cosign could not be run (${r.error.code || r.error.message})` };
|
|
165
|
+
if (r.status === 0) {
|
|
166
|
+
verdict.transparencyLog = { seal: s.name, tlogChecked: !ignoreTlog };
|
|
167
|
+
return accept(s, `the cosign-witnessed ${s.name}`);
|
|
168
|
+
}
|
|
169
|
+
last = firstLine(r.stderr);
|
|
170
|
+
}
|
|
171
|
+
return { sealed: false, reason: `the cosign bundle verifies against no seal in seals/${last ? ` (cosign: ${last})` : ''}` };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function verifyEvidence(dir, { allowed = null, anchorGit = null, anchorRef = null, anchorPin = null, tsr = null, tsaCa = null, cosign = null, maxUnsealed = 0, env = process.env } = {}) {
|
|
72
175
|
const verdict = { verified: true, broken: [], unrecorded: [], open: [], anchored: null, sealed: null, sealedReason: null, unsealedTail: null, signatures: [], seals: [] };
|
|
73
176
|
const ledger = verifyChain(join(dir, LEDGER), LEDGER);
|
|
74
177
|
verdict.broken.push(...ledger.broken);
|
|
@@ -107,7 +210,7 @@ export function verifyEvidence(dir, { allowed = null, anchorGit = null, anchorRe
|
|
|
107
210
|
try { s = decodeSeal(text); } catch (e) { verdict.broken.push({ file: `seals/${f.name}`, line: 1, check: 'seal', reason: e.message }); prevDigest = sha256(text); continue; }
|
|
108
211
|
if (s.previous !== prevDigest) verdict.broken.push({ file: `seals/${f.name}`, line: 1, check: 'seal-chain' });
|
|
109
212
|
prevDigest = sha256(text);
|
|
110
|
-
local.push({ ...s, seq: f.seq, name: f.name, text });
|
|
213
|
+
local.push({ ...s, seq: f.seq, name: f.name, path: f.path, text });
|
|
111
214
|
}
|
|
112
215
|
if (local.length) {
|
|
113
216
|
const newest = local[local.length - 1];
|
|
@@ -120,45 +223,20 @@ export function verifyEvidence(dir, { allowed = null, anchorGit = null, anchorRe
|
|
|
120
223
|
verdict.signatures.push({ seal: s.name, results: sigs });
|
|
121
224
|
}
|
|
122
225
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
verdict.sealedReason = `the witness holds seals for ledger ${[...held.keys()].join(', ')}, and this directory holds ledger ${chain || 'none'}`;
|
|
138
|
-
} else if (!seqs.length) verdict.sealedReason = `not on ${ref}: the witness holds no seal for ledger ${chain || 'none'}`;
|
|
139
|
-
for (const seq of verdict.sealed === false ? [] : seqs) {
|
|
140
|
-
const name = `${seq}.dsse.json`;
|
|
141
|
-
const w = readWitnessed(anchorGit, ref, `${chain}/${name}`, env);
|
|
142
|
-
let ws;
|
|
143
|
-
try { ws = decodeSeal(w.text); } catch (e) { verdict.sealed = false; verdict.sealedReason = `the witnessed ${name} is not a seal: ${e.message}`; break; }
|
|
144
|
-
const a = agrees(ws, recs);
|
|
145
|
-
if (!a.ok) { verdict.sealed = false; verdict.sealedReason = `witnessed ${name}: ${a.why}`; break; }
|
|
146
|
-
const sigs = signaturesOf(ws, allowed);
|
|
147
|
-
const good = sigs.some((x) => x.valid === true);
|
|
148
|
-
const bad = sigs.every((x) => x.valid === false);
|
|
149
|
-
verdict.seals.push({ seal: name, witnessed: true, local: localSeqs.has(seq), length: ws.length, signed: good });
|
|
150
|
-
if (bad) { verdict.sealed = false; verdict.sealedReason = `witnessed ${name} carries no signature from an allowed signer`; break; }
|
|
151
|
-
if (good) newestOk = ws;
|
|
152
|
-
}
|
|
153
|
-
for (const s of local) if (s.chain === chain && !seqs.includes(s.seq)) verdict.seals.push({ seal: s.name, witnessed: false, reason: `not on ${ref}` });
|
|
154
|
-
if (verdict.sealed !== false) {
|
|
155
|
-
if (newestOk) {
|
|
156
|
-
verdict.sealed = true;
|
|
157
|
-
verdict.unsealedTail = recs.length - newestOk.length;
|
|
158
|
-
} else if (!verdict.sealedReason) verdict.sealedReason = allowed ? 'no witnessed seal carries a valid signature' : 'no allowed signers were given';
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} else verdict.sealedReason = 'no witness was named';
|
|
226
|
+
const accept = accepting(recs, allowed);
|
|
227
|
+
const results = [];
|
|
228
|
+
if (anchorGit) results.push(gitWitness({ repo: anchorGit, ref: anchorRef || '@{upstream}', pin: anchorPin, recs, local, accept, allowed, verdict, env }));
|
|
229
|
+
if (tsr) results.push(tsrWitness({ dir, tsr, ca: tsaCa, local, accept, verdict, env }));
|
|
230
|
+
if (cosign) results.push(cosignWitness({ ...cosign, local, accept, verdict, env }));
|
|
231
|
+
const contradicted = results.find((r) => r.sealed === false);
|
|
232
|
+
const confirmed = results.filter((r) => r.sealed === true);
|
|
233
|
+
if (contradicted) {
|
|
234
|
+
verdict.sealed = false;
|
|
235
|
+
verdict.sealedReason = contradicted.reason;
|
|
236
|
+
} else if (confirmed.length) {
|
|
237
|
+
verdict.sealed = true;
|
|
238
|
+
verdict.unsealedTail = recs.length - Math.max(...confirmed.map((r) => r.seal.length));
|
|
239
|
+
} else verdict.sealedReason = results.length ? results.map((r) => r.reason).join('; ') : 'no witness was named';
|
|
162
240
|
|
|
163
241
|
verdict.verified = verdict.broken.length === 0;
|
|
164
242
|
verdict.exit = !verdict.verified || verdict.sealed === false ? 1
|
|
@@ -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'
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// DC and DS operands, and literals, which are DC operands after an equals sign: duplication factor,
|
|
3
|
+
// type and type extension, program type, length, scale and exponent modifiers, and nominal value.
|
|
4
|
+
// https://www.ibm.com/docs/en/hla-and-tf/1.6.0?topic=statements-dc-instruction
|
|
5
|
+
import { HlasmSyntax, splitOperands } from '../operands.mjs';
|
|
6
|
+
import { parseExpression, readExpression } from '../expr.mjs';
|
|
7
|
+
|
|
8
|
+
const EXTENSIONS = {
|
|
9
|
+
C: 'AEU', G: '', X: '', B: '', F: 'D', H: '', E: 'HBD', D: 'HBD', L: 'HBDQ', P: '', Z: '',
|
|
10
|
+
A: 'D', Y: '', S: 'Y', V: 'D', Q: 'DY', J: 'D', R: 'D',
|
|
11
|
+
};
|
|
12
|
+
const ADDRESS_TYPES = new Set(['A', 'Y', 'S', 'V', 'Q', 'J', 'R']);
|
|
13
|
+
|
|
14
|
+
// A parenthesised expression or a decimal number, as a duplication factor or a modifier is written.
|
|
15
|
+
function amount(text, pos) {
|
|
16
|
+
if (text[pos] === '(') {
|
|
17
|
+
const { e, end } = readExpression(text, pos + 1);
|
|
18
|
+
if (text[end] !== ')') throw new HlasmSyntax(`expected ) at column ${end + 1} of '${text}'`);
|
|
19
|
+
return { value: { t: 'paren', e }, end: end + 1 };
|
|
20
|
+
}
|
|
21
|
+
const m = /^\d+/.exec(text.slice(pos));
|
|
22
|
+
if (!m) return null;
|
|
23
|
+
return { value: { t: 'num', v: Number(m[0]) }, end: pos + m[0].length };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function quotedNominal(text, pos) {
|
|
27
|
+
let out = '';
|
|
28
|
+
for (let i = pos + 1; i < text.length; i++) {
|
|
29
|
+
if (text[i] === "'") {
|
|
30
|
+
if (text[i + 1] === "'") { out += "'"; i++; continue; }
|
|
31
|
+
return { value: out, end: i + 1 };
|
|
32
|
+
}
|
|
33
|
+
if (text[i] === '&' && text[i + 1] === '&') i++;
|
|
34
|
+
out += text[i];
|
|
35
|
+
}
|
|
36
|
+
throw new HlasmSyntax(`a nominal value is not closed in '${text}'`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function addressNominal(text, pos) {
|
|
40
|
+
const items = [];
|
|
41
|
+
let at = pos + 1;
|
|
42
|
+
for (;;) {
|
|
43
|
+
const { e, end } = readExpression(text, at);
|
|
44
|
+
at = end;
|
|
45
|
+
if (text[at] === '(') {
|
|
46
|
+
const close = text.indexOf(')', at);
|
|
47
|
+
if (close < 0) throw new HlasmSyntax(`a base register is not closed in '${text}'`);
|
|
48
|
+
items.push({ t: 'address', disp: e, base: text.slice(at + 1, close) });
|
|
49
|
+
at = close + 1;
|
|
50
|
+
} else items.push(e);
|
|
51
|
+
if (text[at] === ',') { at++; continue; }
|
|
52
|
+
if (text[at] === ')') return { value: items, end: at + 1 };
|
|
53
|
+
throw new HlasmSyntax(`expected , or ) at column ${at + 1} of '${text}'`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// One DC operand starting at pos: the operand, and where it ended. A literal needs a nominal value
|
|
58
|
+
// and a duplication factor that is not zero.
|
|
59
|
+
export function readDataOperand(text, pos = 0, { literal = false, nominalRequired = literal } = {}) {
|
|
60
|
+
const operand = { dup: null, type: null, ext: null, program: null, length: null, bitLength: false, scale: null, exponent: null, nominal: null };
|
|
61
|
+
let at = pos;
|
|
62
|
+
const dup = amount(text, at);
|
|
63
|
+
if (dup) { operand.dup = dup.value; at = dup.end; }
|
|
64
|
+
const type = (text[at] || '').toUpperCase();
|
|
65
|
+
if (!(type in EXTENSIONS)) throw new HlasmSyntax(`'${text.slice(pos)}' has no constant type a DC operand can have`);
|
|
66
|
+
operand.type = type;
|
|
67
|
+
at++;
|
|
68
|
+
const ext = (text[at] || '').toUpperCase();
|
|
69
|
+
if (ext && EXTENSIONS[type].includes(ext)) { operand.ext = ext; at++; }
|
|
70
|
+
if ((text[at] || '').toUpperCase() === 'P' && text[at + 1] === '(') {
|
|
71
|
+
const p = amount(text, at + 1);
|
|
72
|
+
operand.program = p.value;
|
|
73
|
+
at = p.end;
|
|
74
|
+
}
|
|
75
|
+
for (const [letter, key] of [['L', 'length'], ['S', 'scale'], ['E', 'exponent']]) {
|
|
76
|
+
if ((text[at] || '').toUpperCase() !== letter) continue;
|
|
77
|
+
let from = at + 1;
|
|
78
|
+
if (key === 'length' && text[from] === '.') { operand.bitLength = true; from++; }
|
|
79
|
+
const sign = key !== 'length' && (text[from] === '-' || text[from] === '+') ? text[from++] : '';
|
|
80
|
+
const m = amount(text, from);
|
|
81
|
+
if (!m) throw new HlasmSyntax(`the ${key} modifier in '${text}' has no value`);
|
|
82
|
+
operand[key] = sign === '-' ? { t: 'unary', op: '-', e: m.value } : m.value;
|
|
83
|
+
at = m.end;
|
|
84
|
+
}
|
|
85
|
+
if (text[at] === "'") {
|
|
86
|
+
if (ADDRESS_TYPES.has(type)) throw new HlasmSyntax(`a ${type}-type constant takes its value in parentheses, not quotes, in '${text}'`);
|
|
87
|
+
const n = quotedNominal(text, at);
|
|
88
|
+
operand.nominal = { quoted: n.value };
|
|
89
|
+
at = n.end;
|
|
90
|
+
} else if (text[at] === '(') {
|
|
91
|
+
if (!ADDRESS_TYPES.has(type)) throw new HlasmSyntax(`a ${type}-type constant takes its value in quotes, not parentheses, in '${text}'`);
|
|
92
|
+
const n = addressNominal(text, at);
|
|
93
|
+
operand.nominal = { expressions: n.value };
|
|
94
|
+
at = n.end;
|
|
95
|
+
}
|
|
96
|
+
// A DC operand may omit its nominal value only when its duplication factor is zero.
|
|
97
|
+
const zeroDup = operand.dup && operand.dup.t === 'num' && operand.dup.v === 0;
|
|
98
|
+
if (nominalRequired && !operand.nominal && !(zeroDup && !literal)) throw new HlasmSyntax(`'${text.slice(pos, at)}' has no nominal value`);
|
|
99
|
+
if (literal && operand.dup && operand.dup.t === 'num' && operand.dup.v === 0) throw new HlasmSyntax('a literal cannot have a duplication factor of zero');
|
|
100
|
+
return { operand, end: at };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function operands(st, nominalRequired) {
|
|
104
|
+
const list = splitOperands(st.field || '');
|
|
105
|
+
if (!list.length) throw new HlasmSyntax(`${st.operation} has no operand`);
|
|
106
|
+
return list.map((text) => {
|
|
107
|
+
const { operand, end } = readDataOperand(text, 0, { nominalRequired });
|
|
108
|
+
if (end !== text.length) throw new HlasmSyntax(`unexpected '${text.slice(end)}' after the operand '${text.slice(0, end)}'`);
|
|
109
|
+
return operand;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
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
|
+
|
|
122
|
+
export const parsers = {
|
|
123
|
+
DC: (st) => ({ kind: 'DC', operands: operands(st, true) }),
|
|
124
|
+
DS: (st) => ({ kind: 'DS', operands: operands(st, false) }),
|
|
125
|
+
CCW: ccw,
|
|
126
|
+
CCW0: ccw,
|
|
127
|
+
CCW1: ccw,
|
|
128
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Parsers for HLASM listing instructions: PRINT, TITLE, EJECT, SPACE, CEJECT.
|
|
3
|
+
|
|
4
|
+
import { HlasmSyntax, splitOperands } from '../operands.mjs';
|
|
5
|
+
import { parseExpression } from '../expr.mjs';
|
|
6
|
+
|
|
7
|
+
const PRINT_OPTS = new Set(['ON', 'OFF', 'GEN', 'NOGEN', 'DATA', 'NODATA', 'MCALL', 'NOMCALL', 'MSOURCE', 'NOMSOURCE', 'UHEAD', 'NOUHEAD', 'NOPRINT']);
|
|
8
|
+
|
|
9
|
+
export const parsers = {
|
|
10
|
+
PRINT(st) {
|
|
11
|
+
const ops = splitOperands(st.field);
|
|
12
|
+
if (ops.length === 0) throw new HlasmSyntax('PRINT requires at least one option');
|
|
13
|
+
const options = ops.map(o => o.trim().toUpperCase());
|
|
14
|
+
for (const opt of options) {
|
|
15
|
+
if (!PRINT_OPTS.has(opt)) throw new HlasmSyntax(`unknown PRINT option: ${opt}`);
|
|
16
|
+
}
|
|
17
|
+
return { kind: 'PRINT', options };
|
|
18
|
+
},
|
|
19
|
+
TITLE(st) {
|
|
20
|
+
const ops = splitOperands(st.field);
|
|
21
|
+
if (ops.length !== 1) throw new HlasmSyntax('TITLE requires exactly one operand');
|
|
22
|
+
const raw = ops[0].trim();
|
|
23
|
+
if (!raw.startsWith("'") || !raw.endsWith("'") || raw.length < 2) throw new HlasmSyntax('TITLE operand must be a quoted string');
|
|
24
|
+
const text = raw.slice(1, -1).replace(/''/g, "'").replace(/&&/g, "&");
|
|
25
|
+
return { kind: 'TITLE', name: st.name, text };
|
|
26
|
+
},
|
|
27
|
+
EJECT(st) {
|
|
28
|
+
if (st.field.trim() !== '') throw new HlasmSyntax('EJECT takes no operand');
|
|
29
|
+
return { kind: 'EJECT', value: null };
|
|
30
|
+
},
|
|
31
|
+
SPACE(st) {
|
|
32
|
+
const ops = splitOperands(st.field);
|
|
33
|
+
if (ops.length === 0) return { kind: 'SPACE', value: null };
|
|
34
|
+
if (ops.length > 1) throw new HlasmSyntax('SPACE takes at most one operand');
|
|
35
|
+
return { kind: 'SPACE', value: parseExpression(ops[0].trim()) };
|
|
36
|
+
},
|
|
37
|
+
CEJECT(st) {
|
|
38
|
+
const ops = splitOperands(st.field);
|
|
39
|
+
if (ops.length === 0) return { kind: 'CEJECT', value: null };
|
|
40
|
+
if (ops.length > 1) throw new HlasmSyntax('CEJECT takes at most one operand');
|
|
41
|
+
return { kind: 'CEJECT', value: parseExpression(ops[0].trim()) };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Assembler instructions that write messages, copy source in or shape the input records: MNOTE,
|
|
3
|
+
// COPY, PUNCH, REPRO, AINSERT, ICTL and ISEQ.
|
|
4
|
+
// https://www.ibm.com/docs/en/hla-and-tf/1.6.0?topic=instructions-mnote-instruction
|
|
5
|
+
import { HlasmSyntax, NAME, splitOperands } from '../operands.mjs';
|
|
6
|
+
import { parseExpression } from '../expr.mjs';
|
|
7
|
+
|
|
8
|
+
// A quoted string's text, '' read as one quote and && as one ampersand.
|
|
9
|
+
function quoted(text, what) {
|
|
10
|
+
const m = /^'((?:[^']|'')*)'$/.exec(text.trim());
|
|
11
|
+
if (!m) throw new HlasmSyntax(`${what} needs a quoted string, not ${text}`);
|
|
12
|
+
return m[1].replace(/''/g, "'").replace(/&&/g, '&');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const numbers = (st, min, max) => {
|
|
16
|
+
const ops = splitOperands(st.field || '');
|
|
17
|
+
if (ops.length < min || ops.length > max || ops.some((o) => !/^\d+$/.test(o.trim()))) throw new HlasmSyntax(`${st.operation} takes ${min} to ${max} decimal numbers, not ${st.field || 'none'}`);
|
|
18
|
+
return { kind: st.operation, values: ops.map(Number) };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const parsers = {
|
|
22
|
+
// [severity,]'message': severity is an absolute expression, *, or empty before the comma.
|
|
23
|
+
MNOTE: (st) => {
|
|
24
|
+
const ops = splitOperands(st.field || '');
|
|
25
|
+
if (ops.length < 1 || ops.length > 2) throw new HlasmSyntax(`MNOTE takes a message and an optional severity, not ${ops.length} operands`);
|
|
26
|
+
const [sev, msg] = ops.length === 2 ? ops : [null, ops[0]];
|
|
27
|
+
const severity = sev === null || sev.trim() === '' ? null : sev.trim() === '*' ? '*' : parseExpression(sev.trim());
|
|
28
|
+
return { kind: 'MNOTE', severity, text: quoted(msg, 'MNOTE') };
|
|
29
|
+
},
|
|
30
|
+
COPY: (st) => {
|
|
31
|
+
const member = (st.field || '').trim();
|
|
32
|
+
if (!NAME.test(member)) throw new HlasmSyntax(`COPY names a member, not ${member || 'nothing'}`);
|
|
33
|
+
return { kind: 'COPY', member: member.toUpperCase() };
|
|
34
|
+
},
|
|
35
|
+
PUNCH: (st) => ({ kind: 'PUNCH', text: quoted(st.field || '', 'PUNCH') }),
|
|
36
|
+
REPRO: () => ({ kind: 'REPRO' }),
|
|
37
|
+
// 'record'[,FRONT|BACK]: a record put into the assembler's input stream.
|
|
38
|
+
AINSERT: (st) => {
|
|
39
|
+
const ops = splitOperands(st.field || '');
|
|
40
|
+
if (ops.length !== 2 || !/^(FRONT|BACK)$/i.test(ops[1].trim())) throw new HlasmSyntax('AINSERT takes a quoted record and FRONT or BACK');
|
|
41
|
+
return { kind: 'AINSERT', text: quoted(ops[0], 'AINSERT'), where: ops[1].trim().toUpperCase() };
|
|
42
|
+
},
|
|
43
|
+
ICTL: (st) => numbers(st, 1, 3),
|
|
44
|
+
ISEQ: (st) => (st.field ? numbers(st, 2, 2) : { kind: 'ISEQ', values: [] }),
|
|
45
|
+
};
|