@portll/cobolwork 0.3.0 → 0.5.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 +35 -9
- package/bin/cobolwork.mjs +7 -1
- package/lib/bms.mjs +8 -0
- package/lib/capabilities.mjs +2 -2
- package/lib/cics-commands.mjs +308 -42
- package/lib/consequence.mjs +19 -0
- package/lib/constants.mjs +60 -0
- package/lib/control.mjs +61 -17
- package/lib/csd.mjs +6 -1
- package/lib/dataflow.mjs +107 -27
- package/lib/explain.mjs +31 -5
- package/lib/exploitability.mjs +49 -4
- package/lib/gate.mjs +31 -9
- package/lib/hlasm.mjs +79 -0
- package/lib/kernel/findings.mjs +2 -0
- package/lib/kernel/registry.mjs +6 -0
- package/lib/kernel/source-tree.mjs +1 -0
- package/lib/options.mjs +6 -2
- package/lib/packs.mjs +4 -0
- package/lib/parser.mjs +67 -12
- package/lib/precompile-cics.mjs +9 -3
- package/lib/precompile.mjs +3 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +3 -2
- package/lib/scan.mjs +2 -1
- package/lib/sets/abend.mjs +186 -0
- package/lib/sets/compile.mjs +2 -1
- package/lib/sets/crypto.mjs +110 -0
- package/lib/sets/flow.mjs +18 -0
- package/lib/sets/hidden.mjs +1 -1
- package/lib/sets/hlasm.mjs +121 -0
- package/lib/sets/jcl.mjs +16 -1
- package/lib/sets/recon.mjs +1 -0
- package/lib/sets/web.mjs +99 -4
- package/lib/sources.mjs +12 -2
- package/lib/utilities.mjs +2 -0
- package/lib/verify.mjs +2 -1
- package/lib/words.mjs +377 -17
- package/package.json +1 -1
- package/rules/compliance-cobit2019.json +180 -0
- package/rules/compliance-dora.json +195 -0
- package/rules/compliance-ffiec.json +195 -0
- package/rules/compliance-nist80053.json +195 -0
- package/rules/gitleaks-mainframe.toml +24 -10
- package/rules/hlasm-operations.json +451 -0
- package/rules/icsf-services.json +1178 -0
package/lib/exploitability.mjs
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
// running system confirms one, so a verdict says confirmed only where the estate brings the result
|
|
7
7
|
// of its own test as a witness feed. See docs/spec/reach.md §9.
|
|
8
8
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
9
|
-
import { basename, sep, delimiter } from 'node:path';
|
|
9
|
+
import { basename, dirname, join, resolve, sep, delimiter } from 'node:path';
|
|
10
10
|
import { ALL_RULES } from './kernel/registry.mjs';
|
|
11
11
|
import { EXPLOITABILITY } from './kernel/findings.mjs';
|
|
12
12
|
import { kindsOf } from './consequence.mjs';
|
|
13
13
|
import { WHO } from './sets/flow.mjs';
|
|
14
14
|
import { entryName, accessOfEntry, accessFromEntry, privilegedFromEntry } from './reach.mjs';
|
|
15
|
+
import { verifyEvidence } from './evidence/verify.mjs';
|
|
15
16
|
|
|
16
17
|
export const HANDLING = 'This report joins routes an attacker can drive with the estate\'s own access facts. It holds no source '
|
|
17
18
|
+ '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 '
|
|
@@ -62,9 +63,22 @@ const WITHOUT_A_TEST = {
|
|
|
62
63
|
// response code is not input, so no test on it clears a route; the fix is not to send it.
|
|
63
64
|
function fixAt(f, kinds) {
|
|
64
65
|
const last = f.trace?.length ? f.trace[f.trace.length - 1] : null;
|
|
66
|
+
const instead = WITHOUT_A_TEST[kinds.sink];
|
|
67
|
+
// A value a STRING built holds the input as one part among literals, where no allow-list or class
|
|
68
|
+
// test describes the whole. The test belongs on the field folded in, before the STRING.
|
|
69
|
+
const built = f.trace ? f.trace.findLastIndex((h) => /^STRING at /.test(h.via || '')) : -1;
|
|
70
|
+
const part = built > 0 ? f.trace[built - 1] : null;
|
|
71
|
+
const where = part?.item && /\bat (.+):(\d+)$/.exec(f.trace[built].via);
|
|
72
|
+
if (where && kinds.source !== 'system-response' && TESTS[kinds.sink]) {
|
|
73
|
+
const [, path, line] = where;
|
|
74
|
+
return {
|
|
75
|
+
program: f.trace[built].program || f.program || null, path, line: Number(line), item: part.item,
|
|
76
|
+
builtInto: { item: last.item, path: f.path, line: f.line },
|
|
77
|
+
test: `before the STRING at ${path}:${line} that builds ${last.item}, on every route to it, test ${TESTS[kinds.sink](part.item)}, and let only a value that passes reach it${instead ? `; ${instead} needs no test` : ''}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
65
80
|
const item = last?.item || 'the value';
|
|
66
81
|
const test = kinds.source === 'system-response' ? null : TESTS[kinds.sink]?.(item) || null;
|
|
67
|
-
const instead = WITHOUT_A_TEST[kinds.sink];
|
|
68
82
|
return {
|
|
69
83
|
program: f.program || null, path: f.path, line: f.line, item,
|
|
70
84
|
...(test ? { test: `before the operation at ${f.path}:${f.line}, on every route to it, test ${test}, and let only a value that passes reach it${instead ? `; ${instead} needs no test` : ''}` }
|
|
@@ -156,6 +170,28 @@ const OUTCOMES = new Set(['reproduced', 'not-reproduced']);
|
|
|
156
170
|
const str = (v) => typeof v === 'string' && v.length > 0;
|
|
157
171
|
const isoDate = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v);
|
|
158
172
|
|
|
173
|
+
// What an ironwork run a result cites shows: each operation the marker reached and the abend the run
|
|
174
|
+
// ended with, from a journal in an evidence directory that verifies. A string says why it shows nothing.
|
|
175
|
+
function witnessedRun(ev, base, verdicts) {
|
|
176
|
+
if (!ev || typeof ev !== 'object' || !str(ev.dir) || !str(ev.run)) return 'cites evidence without a directory and a run';
|
|
177
|
+
if (!/^[0-9TZ]+-[0-9a-f]{16}$/.test(ev.run)) return `cites run ${ev.run}, which is not a run id`;
|
|
178
|
+
const dir = resolve(base, ev.dir);
|
|
179
|
+
if (!verdicts.has(dir)) verdicts.set(dir, verifyEvidence(dir));
|
|
180
|
+
const v = verdicts.get(dir);
|
|
181
|
+
if (!v.verified) return `cites evidence in ${ev.dir} that does not verify`;
|
|
182
|
+
if (v.unrecorded.includes(ev.run)) return `cites run ${ev.run}, which the ledger in ${ev.dir} does not record`;
|
|
183
|
+
let text;
|
|
184
|
+
try { text = readFileSync(join(dir, 'runs', `${ev.run}.jsonl`), 'utf8'); } catch { return `cites run ${ev.run}, which ${ev.dir} does not hold`; }
|
|
185
|
+
const records = [];
|
|
186
|
+
for (const line of text.split('\n')) {
|
|
187
|
+
if (!line) continue;
|
|
188
|
+
const rec = JSON.parse(line);
|
|
189
|
+
if (rec.kind === 'sink' && rec.reached === true) records.push({ kind: 'sink', sink: rec.sink, file: rec.file, line: rec.line });
|
|
190
|
+
else if (rec.kind === 'abend' && str(rec.file) && Number.isInteger(rec.line)) records.push({ kind: 'abend', code: rec.code, file: rec.file, line: rec.line });
|
|
191
|
+
}
|
|
192
|
+
return records.length ? { run: ev.run, records } : `cites run ${ev.run}, whose journal shows no operation the marker reached and no abend at a line`;
|
|
193
|
+
}
|
|
194
|
+
|
|
159
195
|
// The results of the estate's own tests, keyed by the fingerprint of the finding each tried, loaded
|
|
160
196
|
// for one scan and refused from inside the scanned tree the way a reach feed is: a list of what was
|
|
161
197
|
// reproduced is a list of what to attack. Shape:
|
|
@@ -176,6 +212,7 @@ export function loadWitnessFeed(path, { root = null } = {}) {
|
|
|
176
212
|
if (!doc.results || typeof doc.results !== 'object' || Array.isArray(doc.results)) { feed.problem = 'holds no results'; return feed; }
|
|
177
213
|
feed.witness = doc.witness;
|
|
178
214
|
feed.recorded = doc.recorded;
|
|
215
|
+
const verdicts = new Map();
|
|
179
216
|
for (const [fingerprint, r] of Object.entries(doc.results)) {
|
|
180
217
|
const why = !r || typeof r !== 'object' ? 'is not an object'
|
|
181
218
|
: !OUTCOMES.has(r.outcome) ? `says outcome ${JSON.stringify(r.outcome)}, not "reproduced" or "not-reproduced"`
|
|
@@ -183,7 +220,9 @@ export function loadWitnessFeed(path, { root = null } = {}) {
|
|
|
183
220
|
: !isoDate(r.on) ? 'does not say when the test ran'
|
|
184
221
|
: !str(r.system) ? 'does not say which system it ran on' : null;
|
|
185
222
|
if (why) { feed.refused.push({ fingerprint, why: `the result for ${fingerprint} ${why}` }); continue; }
|
|
186
|
-
|
|
223
|
+
const evidence = r.evidence === undefined ? null : witnessedRun(r.evidence, dirname(real), verdicts);
|
|
224
|
+
if (typeof evidence === 'string') { feed.refused.push({ fingerprint, why: `the result for ${fingerprint} ${evidence}` }); continue; }
|
|
225
|
+
feed.results.set(fingerprint, { outcome: r.outcome, by: r.by, on: r.on, system: r.system, ...(str(r.reference) ? { reference: r.reference } : {}), ...(evidence ? { evidence } : {}) });
|
|
187
226
|
}
|
|
188
227
|
return feed;
|
|
189
228
|
}
|
|
@@ -214,8 +253,11 @@ export function applyWitness(findings, checked, feeds) {
|
|
|
214
253
|
const x = f.exploitability;
|
|
215
254
|
const hit = x && f.fingerprint ? newest(good, f.fingerprint) : null;
|
|
216
255
|
if (!hit) return false;
|
|
217
|
-
matched.add(f.fingerprint);
|
|
218
256
|
const { r, feed } = hit;
|
|
257
|
+
// A cited run witnesses this finding only where its journal places the operation or the abend at the finding's line.
|
|
258
|
+
const at = r.evidence ? r.evidence.records.find((e) => e.line === f.line && basename(e.file) === basename(f.path)) : null;
|
|
259
|
+
if (r.evidence && !at) return false;
|
|
260
|
+
matched.add(f.fingerprint);
|
|
219
261
|
const cite = `${feed.file} (${feed.witness}, recorded ${feed.recorded})`;
|
|
220
262
|
x.witness = { ...r, from: cite };
|
|
221
263
|
byWitness[r.outcome]++;
|
|
@@ -227,6 +269,9 @@ export function applyWitness(findings, checked, feeds) {
|
|
|
227
269
|
const was = x.verdict;
|
|
228
270
|
x.verdict = 'confirmed';
|
|
229
271
|
x.because.push(`reproduced on ${r.system} by ${r.by} on ${r.on}${r.reference ? ` (${r.reference})` : ''}, per ${cite}`);
|
|
272
|
+
if (at) x.because.push(at.kind === 'sink'
|
|
273
|
+
? `run ${r.evidence.run}'s verified journal records the marker reaching the ${at.sink} operation at ${at.file}:${at.line}`
|
|
274
|
+
: `run ${r.evidence.run}'s verified journal records the run ending with ${at.code} at ${at.file}:${at.line}`);
|
|
230
275
|
delete x.unknown;
|
|
231
276
|
if (!x.fixAt) x.fixAt = fixAt(f, kindsOf(f.rule));
|
|
232
277
|
if (f.guardedFrom) f.sev = f.guardedFrom;
|
package/lib/gate.mjs
CHANGED
|
@@ -20,7 +20,9 @@ const MAX_EDIT = 2000;
|
|
|
20
20
|
const MAX_LINES = 200000;
|
|
21
21
|
const MAX_COMPILES = 50;
|
|
22
22
|
const COMPILE_BUDGET_MS = 10 * 60 * 1000;
|
|
23
|
-
|
|
23
|
+
// A fix keeps the program's statements and stops the route: deleting the flagged statement or the
|
|
24
|
+
// source statement removes what the program did, and fails.
|
|
25
|
+
const PASSING = new Set(['cleared-by-check', 'no-longer-fires']);
|
|
24
26
|
const UNDECIDED = new Set(['lowered-by-check', 'gone-unexplained']);
|
|
25
27
|
|
|
26
28
|
const pathText = (p) => printable(String(p ?? ''), 120);
|
|
@@ -115,15 +117,18 @@ export function findCobc({ explicit = null, repo, env = process.env, platform =
|
|
|
115
117
|
return found.path ? found : { path: null, why: `--cobc ${pathText(resolve(explicit))} is not a file outside the repository` };
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
// A compiler run as `cobc -fsyntax-only`, from outside the tree, for a bounded time.
|
|
120
|
+
// A compiler run as `cobc -fsyntax-only`, from outside the tree, for a bounded time. Its error lines
|
|
121
|
+
// are kept so a drafter whose patch stopped a program compiling is told where.
|
|
119
122
|
export function cobcCompiler(cobc) {
|
|
120
123
|
return (file, { includeDirs = [], free = false } = {}) => {
|
|
121
124
|
const r = spawnSync(cobc, ['-fsyntax-only', ...(free ? ['-free'] : []), ...includeDirs.flatMap((d) => ['-I', d]), file],
|
|
122
|
-
{ cwd: tmpdir(), timeout: 60000, windowsHide: true, stdio: 'ignore' });
|
|
123
|
-
return r.status === 0;
|
|
125
|
+
{ cwd: tmpdir(), timeout: 60000, windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'], encoding: 'latin1', maxBuffer: 1 << 20 });
|
|
126
|
+
return { ok: r.status === 0, messages: String(r.stderr || '').split(/\r?\n/).filter((l) => /: error: /.test(l)) };
|
|
124
127
|
};
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
const MAX_COMPILER_LINES = 3;
|
|
131
|
+
|
|
127
132
|
// Level 1 matches across both lists first; each looser level pairs only what the level above left.
|
|
128
133
|
export function pairLists(base, head, baseKeys, headKeys) {
|
|
129
134
|
const headOf = new Array(base.length).fill(-1);
|
|
@@ -222,6 +227,10 @@ function targetReason(t, r) {
|
|
|
222
227
|
return `the target is still reported, lowered from ${t.sev} to ${r.head.sev} by a check this patch added at ${where({ path: r.guard.file, line: r.guard.line })}; a check that lowers rather than stops may not turn away everything it should, so a person decides`;
|
|
223
228
|
case 'program-removed':
|
|
224
229
|
return `the patch removes ${pathText(t.path)}${t.program ? ` or its program ${printable(t.program, 40)}` : ''}, which held the target; removing the program is not a fix the gate accepts`;
|
|
230
|
+
case 'statement-removed':
|
|
231
|
+
return `the patch deletes the target's statement at ${where(t)}; a fix keeps the statement and stops the route to it, so deleting it is not a fix the gate accepts`;
|
|
232
|
+
case 'source-removed':
|
|
233
|
+
return `the patch deletes the statement the target's input comes from; a fix keeps it and stops the route from it, so deleting it is not a fix the gate accepts`;
|
|
225
234
|
case 'gone-unexplained':
|
|
226
235
|
return `the target, ${at}, is gone, but the patch neither removed its statement or its source nor added a check that stops its route; the route may be cut between its ends, or sent through something the engine does not follow, and a person decides`;
|
|
227
236
|
default:
|
|
@@ -246,16 +255,29 @@ function compileCheck({ compiler, baseRoot, headRoot, baseProgs, headProgs, chan
|
|
|
246
255
|
try { free = detectFormat(readFileSync(join(root, file), 'latin1')) === 'free'; } catch { /* unreadable: compiled as fixed */ }
|
|
247
256
|
return { includeDirs: [...dirs].sort(), free };
|
|
248
257
|
};
|
|
258
|
+
// A compiler answers true or false, or { ok, messages }.
|
|
259
|
+
const compile = (root, progs, file) => {
|
|
260
|
+
const r = compiler.run(join(root, file), optsFor(root, progs, file));
|
|
261
|
+
return r && typeof r === 'object' ? { ok: !!r.ok, messages: r.messages || [] } : { ok: !!r, messages: [] };
|
|
262
|
+
};
|
|
249
263
|
const regressed = [], neverCompiled = [];
|
|
250
264
|
const deadline = Date.now() + (compiler.budgetMs ?? COMPILE_BUDGET_MS);
|
|
251
265
|
for (const file of [...files].sort()) {
|
|
252
266
|
if (Date.now() > deadline) return { ok: null, compiled: `not compiled: the compiler ran past the gate's ${Math.round((compiler.budgetMs ?? COMPILE_BUDGET_MS) / 60000)}-minute budget`, reasons: [] };
|
|
253
|
-
const now =
|
|
254
|
-
if (now) continue;
|
|
255
|
-
const had = existsSync(join(baseRoot, file)) &&
|
|
256
|
-
(had
|
|
267
|
+
const now = compile(headRoot, headProgs, file);
|
|
268
|
+
if (now.ok) continue;
|
|
269
|
+
const had = existsSync(join(baseRoot, file)) && compile(baseRoot, baseProgs, file).ok;
|
|
270
|
+
if (had) regressed.push({ file, messages: now.messages });
|
|
271
|
+
else neverCompiled.push(file);
|
|
272
|
+
}
|
|
273
|
+
// The compiler names the file by where the head was written; the reader knows it by its path.
|
|
274
|
+
const asInRepo = (m) => m.split(join(headRoot, sep)).join('');
|
|
275
|
+
if (regressed.length) {
|
|
276
|
+
return { ok: false, compiled: `${regressed.length} program(s) compiled before the patch and do not after`, reasons: regressed.flatMap(({ file, messages }) => [
|
|
277
|
+
`${pathText(file)} compiled before the patch and does not after`,
|
|
278
|
+
...messages.slice(0, MAX_COMPILER_LINES).map((m) => `the compiler: ${asInRepo(m).trim()}`),
|
|
279
|
+
]) };
|
|
257
280
|
}
|
|
258
|
-
if (regressed.length) return { ok: false, compiled: `${regressed.length} program(s) compiled before the patch and do not after`, reasons: regressed.map((f) => `${pathText(f)} compiled before the patch and does not after`) };
|
|
259
281
|
if (neverCompiled.length) return { ok: null, compiled: `${neverCompiled.length} program(s) do not compile before or after the patch, or are new and do not compile, which the gate cannot tell from a missing copy library`, reasons: [] };
|
|
260
282
|
return { ok: true, compiled: `${files.size} program(s) compile`, reasons: [] };
|
|
261
283
|
}
|
package/lib/hlasm.mjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// HLASM source, read and not assembled. The cards fold into statements as the assembler folds them
|
|
3
|
+
// (lib/bms.mjs), and each statement's operation is looked up in rules/hlasm-operations.json, the
|
|
4
|
+
// operations that matter to a reviewer, each with the IBM document that defines it. Macros are not
|
|
5
|
+
// expanded and conditional assembly is not evaluated, so an operation is reported where it is
|
|
6
|
+
// written: in a macro definition, or in a branch AIF jumps over, as much as anywhere else.
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { foldStatements } from './bms.mjs';
|
|
9
|
+
import { parseOperands, splitOperands } from './cards.mjs';
|
|
10
|
+
|
|
11
|
+
const TABLE = JSON.parse(readFileSync(new URL('../rules/hlasm-operations.json', import.meta.url), 'utf8'));
|
|
12
|
+
export const OPERATIONS = new Map(TABLE.operations.map((o) => [o.name, o]));
|
|
13
|
+
|
|
14
|
+
// Operations only assembler source holds: sections, base registers, the instructions every
|
|
15
|
+
// program's linkage uses, and macro definitions. x86 or 6502 source written in a .asm file has none.
|
|
16
|
+
// The weak ones are words another assembler's source can hold (a 6502 `zproc start`), so it takes
|
|
17
|
+
// two different ones to count. USING counts only with the comma between its base and register: a
|
|
18
|
+
// comment line written `C Using a ...`, as GMP's are, reads as label C and operation USING.
|
|
19
|
+
const ASSEMBLER = new Set(['CSECT', 'DSECT', 'RSECT', 'USING', 'BALR', 'BASR', 'STM', 'STMG', 'LM', 'LMG', 'MVC',
|
|
20
|
+
'MACRO', 'MEND', 'AMODE', 'RMODE', 'LTORG', 'CNOP']);
|
|
21
|
+
const WEAK = new Set(['START', 'DC', 'DS', 'BAL', 'BAS', 'DROP']);
|
|
22
|
+
const strong = (s) => ASSEMBLER.has(s.operation) && (s.operation !== 'USING' || s.field.includes(','));
|
|
23
|
+
|
|
24
|
+
// GNU assembler source, run through m4 as GMP's is, holds s390 instructions too, and is not HLASM.
|
|
25
|
+
const GNU = /^dnl\b|^\s*\.(?:text|globl|global|section|align|file)\b|\b(?:PROLOGUE|EPILOGUE|ASM_START)\(/m;
|
|
26
|
+
|
|
27
|
+
// The macros that make a file a BMS map or an IMS database or program definition, which their own
|
|
28
|
+
// readers take.
|
|
29
|
+
const BMS = new Set(['DFHMSD', 'DFHMDI', 'DFHMDF']);
|
|
30
|
+
const IMS = new Set(['DBD', 'DBDGEN', 'SEGM', 'PSBGEN', 'PCB', 'SENSEG']);
|
|
31
|
+
|
|
32
|
+
// MODESET changes the PSW key or the state; these operand values make it key zero or supervisor
|
|
33
|
+
// state, and the opposite values (KEY=NZERO, MODE=PROB) return from it. EXTKEY is the inline form,
|
|
34
|
+
// which sets only the PSW key.
|
|
35
|
+
const STATE_CHANGE = { KEY: ['ZERO'], MODE: ['SUP'], EXTKEY: ['ZERO'] };
|
|
36
|
+
|
|
37
|
+
const nameList = (field) => splitOperands(field || '').map((s) => s.trim().toUpperCase()).filter((s) => /^[A-Z$#@_][A-Z0-9$#@_]*$/.test(s));
|
|
38
|
+
|
|
39
|
+
function stateChange(field) {
|
|
40
|
+
let ops;
|
|
41
|
+
try { ops = parseOperands(field || ''); } catch { return null; }
|
|
42
|
+
for (const [key, values] of Object.entries(STATE_CHANGE)) {
|
|
43
|
+
const v = ops.keywords.get(key);
|
|
44
|
+
if (v && values.includes(String(v).trim().toUpperCase())) return `${key}=${String(v).trim().toUpperCase()}`;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// What the file is, and in it: each operation the table names with where it is written; the
|
|
50
|
+
// sections and entry points it defines, which a CALL or a job step can name; and the names it
|
|
51
|
+
// leaves to the binder.
|
|
52
|
+
export function readHlasm(text) {
|
|
53
|
+
const { statements, diags } = foldStatements(text);
|
|
54
|
+
const ops = statements.filter((s) => s.kind === 'statement');
|
|
55
|
+
const has = (set) => ops.some((s) => set.has(s.operation));
|
|
56
|
+
const weak = new Set(ops.filter((s) => WEAK.has(s.operation)).map((s) => s.operation)).size;
|
|
57
|
+
const assembler = !GNU.test(text) && (ops.some(strong) || weak >= 2);
|
|
58
|
+
const kind = has(BMS) ? 'bms' : has(IMS) ? 'ims' : assembler ? 'hlasm' : 'unrecognised';
|
|
59
|
+
const out = { kind, statements: ops.length, operations: [], defines: [], external: [], diags };
|
|
60
|
+
if (kind !== 'hlasm') return out;
|
|
61
|
+
|
|
62
|
+
let macroDepth = 0;
|
|
63
|
+
for (const s of ops) {
|
|
64
|
+
if (s.operation === 'MACRO') { macroDepth++; continue; }
|
|
65
|
+
if (s.operation === 'MEND') { macroDepth = Math.max(0, macroDepth - 1); continue; }
|
|
66
|
+
const inMacro = macroDepth > 0;
|
|
67
|
+
if (!inMacro && ['CSECT', 'RSECT', 'START'].includes(s.operation) && s.name && !s.name.startsWith('&')) {
|
|
68
|
+
out.defines.push({ name: s.name.toUpperCase(), how: s.operation, line: s.line });
|
|
69
|
+
}
|
|
70
|
+
if (!inMacro && s.operation === 'ENTRY') for (const name of nameList(s.field)) out.defines.push({ name, how: 'ENTRY', line: s.line });
|
|
71
|
+
if (!inMacro && (s.operation === 'EXTRN' || s.operation === 'WXTRN')) for (const name of nameList(s.field)) out.external.push({ name, how: s.operation, line: s.line });
|
|
72
|
+
const op = OPERATIONS.get(s.operation);
|
|
73
|
+
if (!op || op.class === 'section' || op.class === 'linkage') continue;
|
|
74
|
+
const use = { name: op.name, class: op.class, line: s.line, inMacro, field: s.field };
|
|
75
|
+
if (op.name === 'MODESET') use.stateChange = stateChange(s.field);
|
|
76
|
+
out.operations.push(use);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
package/lib/kernel/findings.mjs
CHANGED
|
@@ -38,6 +38,7 @@ export const EVIDENCE = Object.freeze({
|
|
|
38
38
|
advisory: 'a component version matches a published advisory against it',
|
|
39
39
|
exposure: 'information about the estate is written into the source',
|
|
40
40
|
change: 'a change moves an interface or adds a call target, which a reviewer has to look at',
|
|
41
|
+
execution: 'a run of the program on a recorded input ended this way, and the run\'s verified journal is the record of it',
|
|
41
42
|
coverage: 'the analysis stopped following here; a limit of the tool, not a defect in the code',
|
|
42
43
|
context: 'describes the estate - an entry point, a product in use - and asserts no defect',
|
|
43
44
|
});
|
|
@@ -60,6 +61,7 @@ export const WHO_ACTS = Object.freeze({
|
|
|
60
61
|
advisory: 'whoever owns the build',
|
|
61
62
|
exposure: 'the owner',
|
|
62
63
|
change: 'the reviewer of that change',
|
|
64
|
+
execution: "the program's owner",
|
|
63
65
|
coverage: "nobody's code: read more, or accept the limit",
|
|
64
66
|
context: 'nobody: it asserts no defect',
|
|
65
67
|
});
|
package/lib/kernel/registry.mjs
CHANGED
|
@@ -27,6 +27,9 @@ import { scanLog, LOG_RULES } from '../sets/log.mjs';
|
|
|
27
27
|
import { scanCompile, COMPILE_RULES } from '../sets/compile.mjs';
|
|
28
28
|
import { scanSemantics, SEMANTICS_RULES } from '../sets/semantics.mjs';
|
|
29
29
|
import { scanZowe, ZOWE_RULES } from '../sets/zowe.mjs';
|
|
30
|
+
import { scanAbend, ABEND_RULES } from '../sets/abend.mjs';
|
|
31
|
+
import { scanHlasm, HLASM_RULES } from '../sets/hlasm.mjs';
|
|
32
|
+
import { scanCrypto, CRYPTO_RULES } from '../sets/crypto.mjs';
|
|
30
33
|
import { toolName } from './ruleset.mjs';
|
|
31
34
|
|
|
32
35
|
// The order is the order a scan runs them and the order --only lists them. It is not significant
|
|
@@ -47,6 +50,9 @@ export const REGISTRY = [
|
|
|
47
50
|
{ name: 'log', scan: scanLog, rules: LOG_RULES },
|
|
48
51
|
{ name: 'semantics', scan: scanSemantics, rules: SEMANTICS_RULES },
|
|
49
52
|
{ name: 'zowe', scan: scanZowe, rules: ZOWE_RULES },
|
|
53
|
+
{ name: 'abend', scan: scanAbend, rules: ABEND_RULES },
|
|
54
|
+
{ name: 'hlasm', scan: scanHlasm, rules: HLASM_RULES },
|
|
55
|
+
{ name: 'crypto', scan: scanCrypto, rules: CRYPTO_RULES },
|
|
50
56
|
];
|
|
51
57
|
|
|
52
58
|
// Re-exported from the kernel's ruleset module, which is where it has to live: this file imports
|
|
@@ -154,6 +154,7 @@ function heldTree({ kind, root, store, rel, systemDirs = [], copyDirs = null, cl
|
|
|
154
154
|
// Buffer. Paths are used exactly as given, so a test reads the way it writes.
|
|
155
155
|
export function memoryTree(files, { root = '/memory', systemDirs = [] } = {}) {
|
|
156
156
|
const store = new Map(Object.entries(files).map(([p, v]) => [p, Buffer.isBuffer(v) ? v : Buffer.from(v, 'latin1')]));
|
|
157
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- root is escaped
|
|
157
158
|
const rel = (p) => String(p).replace(/\\/g, '/').replace(new RegExp('^' + root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/?'), '');
|
|
158
159
|
return heldTree({ kind: 'memory', root, store, rel, systemDirs });
|
|
159
160
|
}
|
package/lib/options.mjs
CHANGED
|
@@ -433,7 +433,11 @@ export function compilerTasks(text) {
|
|
|
433
433
|
};
|
|
434
434
|
const valueOf = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v.value : v);
|
|
435
435
|
const folder = (v) => String(v).replace(/\$\{(?:workspaceFolder|workspaceRoot)(?::[^}]*)?\}/g, '.').replace(/\$\{(?:\/|pathSeparator)\}/g, '/');
|
|
436
|
-
const quoted = (a) => {
|
|
436
|
+
const quoted = (a, dialect) => {
|
|
437
|
+
const s = String(valueOf(a) ?? '');
|
|
438
|
+
const esc = ESCAPE[dialect];
|
|
439
|
+
return /[\s"]/.test(s) || s === '' ? `"${s.replace(/./gs, (c) => (c === '"' || c === esc ? esc + c : c))}"` : s;
|
|
440
|
+
};
|
|
437
441
|
const out = [];
|
|
438
442
|
const tasks = [doc, ...(Array.isArray(doc && doc.tasks) ? doc.tasks : [])];
|
|
439
443
|
for (const task of tasks) {
|
|
@@ -448,7 +452,7 @@ export function compilerTasks(text) {
|
|
|
448
452
|
const line = lineOf(JSON.stringify(command).slice(1, 41), task.label && JSON.stringify(task.label));
|
|
449
453
|
const state = { dir };
|
|
450
454
|
const found = [];
|
|
451
|
-
commandLine(folder([command, ...args.map(quoted)].join(' ')), dialect, state, line, found);
|
|
455
|
+
commandLine(folder([command, ...args.map((a) => quoted(a, dialect))].join(' ')), dialect, state, line, found);
|
|
452
456
|
out.push(...found);
|
|
453
457
|
}
|
|
454
458
|
}
|
package/lib/packs.mjs
CHANGED
|
@@ -113,8 +113,10 @@ export function packProblems(pack) {
|
|
|
113
113
|
const scopes = r.appliesTo || ['jcl-instream'];
|
|
114
114
|
for (const sc of scopes) if (!SCOPES.includes(sc)) problems.push(`${pack.name}:${r.id}: unknown scope '${sc}'`);
|
|
115
115
|
if (!scopes.length) problems.push(`${pack.name}:${r.id}: no scope, so it can never match anything`);
|
|
116
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- a shipped pack's pattern, checked here to compile
|
|
116
117
|
try { new RegExp(r.pattern, r.flags || 'i'); } catch (e) { problems.push(`${pack.name}:${r.id}: ${e.message}`); }
|
|
117
118
|
if (r.setsContext && !r.setsContextPattern) problems.push(`${pack.name}:${r.id}: setsContext without a pattern that sets it`);
|
|
119
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- a shipped pack's pattern, checked here to compile
|
|
118
120
|
if (r.setsContextPattern) { try { new RegExp(r.setsContextPattern, 'i'); } catch (e) { problems.push(`${pack.name}:${r.id}: setsContextPattern ${e.message}`); } }
|
|
119
121
|
// A rule that waits for a context nothing in the pack sets can never fire, which is the same
|
|
120
122
|
// failure as an unknown scope and is caught the same way.
|
|
@@ -139,10 +141,12 @@ export function compilePack(pack) {
|
|
|
139
141
|
pack: pack.name,
|
|
140
142
|
vendor: pack.vendor,
|
|
141
143
|
product: pack.product,
|
|
144
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- a shipped pack's pattern; test/hostile times each one
|
|
142
145
|
re: new RegExp(r.pattern, r.flags || 'i'),
|
|
143
146
|
appliesTo: r.appliesTo || ['jcl-instream'],
|
|
144
147
|
// A rule may declare that it only means what it says once an earlier line has put the stream
|
|
145
148
|
// into a mode, and a rule may be the thing that sets that mode.
|
|
149
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- a shipped pack's pattern; test/hostile times each one
|
|
146
150
|
setsContextRe: r.setsContext && r.setsContextPattern ? new RegExp(r.setsContextPattern, 'i') : null,
|
|
147
151
|
}));
|
|
148
152
|
}
|
package/lib/parser.mjs
CHANGED
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
EIB_FIELDS, DIB_FIELDS, SQLCA_FIELDS,
|
|
9
9
|
} from './words.mjs';
|
|
10
10
|
import { CICS_COMMANDS, CICS_EVERY_COMMAND } from './cics-commands.mjs';
|
|
11
|
-
import { cicsCommand } from './precompile-cics.mjs';
|
|
11
|
+
import { cicsCommand, symbolicMapCopybook } from './precompile-cics.mjs';
|
|
12
|
+
import { parseBms } from './bms.mjs';
|
|
12
13
|
import { BINARY_SIZE, literalBytes, computeSizes, layoutReport } from './layout.mjs';
|
|
13
14
|
import { hostVariablesIn } from './embedded-sql.mjs';
|
|
14
15
|
import { join, dirname, basename, resolve, isAbsolute, sep, delimiter } from 'node:path';
|
|
@@ -88,7 +89,7 @@ export function detectFormat(src) {
|
|
|
88
89
|
function codePastColumn72(src) {
|
|
89
90
|
const lines = src.split(/\r?\n/, 2001);
|
|
90
91
|
for (let i = 0; i < Math.min(lines.length, 2000); i++) {
|
|
91
|
-
const l = expandTabs(lines[i]).
|
|
92
|
+
const l = expandTabs(lines[i]).trimEnd();
|
|
92
93
|
if (l.length <= 72 || l[6] === '*' || l[6] === '/' || /^\s*\*>/.test(l) || COMMENT_ENTRY.test(l.slice(7))) continue;
|
|
93
94
|
const { quote, comment } = stateAtColumn72(l);
|
|
94
95
|
if (comment) continue;
|
|
@@ -138,9 +139,42 @@ function scanQuotes(text, open) {
|
|
|
138
139
|
|
|
139
140
|
const PREDEFINED = new Map([['P64', 'SET']]);
|
|
140
141
|
|
|
142
|
+
// Cuts a floating comment where /\*>.*$/ would: at the first *> after the last line break.
|
|
143
|
+
function withoutInlineComment(s) {
|
|
144
|
+
let from = 0;
|
|
145
|
+
for (const b of ['\n', '\r', '\u2028', '\u2029']) from = Math.max(from, s.lastIndexOf(b) + 1);
|
|
146
|
+
const at = s.indexOf('*>', from);
|
|
147
|
+
return at < 0 ? s : s.slice(0, at);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The name, value and OVERRIDE of a >>DEFINE body as /^(?:CONSTANT\s+)?([A-Za-z0-9_-]+)\s+(?:AS\s+)?(.*?)\s*(OVERRIDE)?\s*$/i
|
|
151
|
+
// reads them, in time linear in the body.
|
|
152
|
+
function defineParts(body) {
|
|
153
|
+
const space = (c) => c !== undefined && /\s/.test(c);
|
|
154
|
+
const skip = (i) => { while (space(body[i])) i++; return i; };
|
|
155
|
+
const from = (start) => {
|
|
156
|
+
let j = start;
|
|
157
|
+
while (j < body.length && /[A-Za-z0-9_-]/.test(body[j])) j++;
|
|
158
|
+
if (j === start || !space(body[j])) return null;
|
|
159
|
+
let v = skip(j);
|
|
160
|
+
if (/^AS$/i.test(body.slice(v, v + 2)) && space(body[v + 2])) v = skip(v + 2);
|
|
161
|
+
let end = body.length;
|
|
162
|
+
while (end > v && space(body[end - 1])) end--;
|
|
163
|
+
let override;
|
|
164
|
+
if (end - 8 >= v && /^OVERRIDE$/i.test(body.slice(end - 8, end))) {
|
|
165
|
+
override = body.slice(end - 8, end);
|
|
166
|
+
end -= 8;
|
|
167
|
+
while (end > v && space(body[end - 1])) end--;
|
|
168
|
+
}
|
|
169
|
+
const value = body.slice(v, end);
|
|
170
|
+
return /[\n\r\u2028\u2029]/.test(value) ? null : [body, body.slice(start, j), value, override];
|
|
171
|
+
};
|
|
172
|
+
return (/^CONSTANT$/i.test(body.slice(0, 8)) && space(body[8]) && from(skip(8))) || from(0);
|
|
173
|
+
}
|
|
174
|
+
|
|
141
175
|
function defineDirective(directive, defines) {
|
|
142
|
-
const body = directive
|
|
143
|
-
const m = body
|
|
176
|
+
const body = withoutInlineComment(directive).replace(/^>>\s*DEFINE\s+/i, '').trim();
|
|
177
|
+
const m = defineParts(body);
|
|
144
178
|
if (!m) return;
|
|
145
179
|
const name = m[1].toUpperCase();
|
|
146
180
|
const raw = m[2].trim();
|
|
@@ -153,17 +187,17 @@ function defineDirective(directive, defines) {
|
|
|
153
187
|
function setDirective(text, defines) {
|
|
154
188
|
const c = /\bCONSTANT\s+([A-Za-z0-9_-]+)\s+(?:(["'])(.*?)\2|(\S+))/i.exec(text);
|
|
155
189
|
if (c && defines) defines.set(c[1].toUpperCase(), c[3] !== undefined ? c[3] : c[4]);
|
|
156
|
-
const m = text.match(/SOURCEFORMAT\s
|
|
190
|
+
const m = text.match(/SOURCEFORMAT\s*(?:\(\s*)?["']?(FREE|FIXED|VARIABLE)/i);
|
|
157
191
|
return m ? m[1].toLowerCase() : null;
|
|
158
192
|
}
|
|
159
193
|
|
|
160
194
|
function evaluateCondition(text, defines) {
|
|
161
|
-
const t = text
|
|
195
|
+
const t = withoutInlineComment(text).trim();
|
|
162
196
|
const known = (n) => (defines && defines.has(n)) || PREDEFINED.has(n);
|
|
163
197
|
const valueOf = (n) => (defines && defines.has(n) ? defines.get(n) : PREDEFINED.get(n));
|
|
164
198
|
let m = t.match(/^([A-Za-z0-9_-]+)\s+(?:IS\s+)?(NOT\s+)?(DEFINED|SET)$/i);
|
|
165
199
|
if (m) { const r = known(m[1].toUpperCase()); return m[2] ? !r : r; }
|
|
166
|
-
m = t.match(/^([A-Za-z0-9_-]+)\s*(<=|>=|<>|=|<|>)\s*(['"]?)([^'"]*)\3$/);
|
|
200
|
+
m = t.match(/^([A-Za-z0-9_-]+)\s*(<=|>=|<>|=|<|>)\s*(?!\s)(['"]?)([^'"]*)\3$/);
|
|
167
201
|
if (m) {
|
|
168
202
|
const name = m[1].toUpperCase();
|
|
169
203
|
if (!known(name)) return null;
|
|
@@ -583,7 +617,8 @@ function applyReplacing(tokens, pairs, maxGrowth = Infinity, byCopy = false) {
|
|
|
583
617
|
if (p.mode || p.partial || p.from.toks.length !== 1 || p.to.toks.length > 1) continue;
|
|
584
618
|
const pat = p.from.toks[0].v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
585
619
|
const rep = p.to.toks[0] ? p.to.toks[0].v : '';
|
|
586
|
-
|
|
620
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- pat is escaped
|
|
621
|
+
const v = tk.v.replace(new RegExp(`\\(${pat}\\)`, 'gi'), () => `(${rep})`);
|
|
587
622
|
if (v !== tk.v) tk = { ...tk, v, u: v };
|
|
588
623
|
}
|
|
589
624
|
}
|
|
@@ -790,15 +825,31 @@ function expand(tokens, ctx, stack, format) {
|
|
|
790
825
|
return out;
|
|
791
826
|
}
|
|
792
827
|
|
|
828
|
+
// The symbolic map BMS would generate for a COBOL mapset the tree holds only as BMS source, each
|
|
829
|
+
// line placed at the BMS statement it comes from.
|
|
830
|
+
function symbolicMapFor(name, ctx) {
|
|
831
|
+
const want = name.toUpperCase();
|
|
832
|
+
for (const p of filesByName(ctx.fileIndex).get(`${name}.bms`.toLowerCase()) || []) {
|
|
833
|
+
let mapsets;
|
|
834
|
+
try { ({ mapsets } = parseBms(ctx.readText(p))); } catch { continue; }
|
|
835
|
+
const mapset = mapsets.find((m) => m.name?.toUpperCase() === want && String(m.options?.lang || 'COBOL').toUpperCase() === 'COBOL');
|
|
836
|
+
const copybook = mapset && symbolicMapCopybook(mapset);
|
|
837
|
+
if (copybook) return { path: p, ...copybook };
|
|
838
|
+
}
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
|
|
793
842
|
function includeCopy(name, lib, pairs, at, via, ctx, stack, inheritedFormat) {
|
|
794
|
-
const path = resolveCopy(name, lib, ctx);
|
|
795
843
|
const refused = copyRefusal(name, ctx);
|
|
796
|
-
|
|
844
|
+
let path = resolveCopy(name, lib, ctx);
|
|
845
|
+
const generated = !path && !refused && !lib && via === 'copy' && ctx.fileIndex ? symbolicMapFor(name, ctx) : null;
|
|
846
|
+
if (generated) path = generated.path;
|
|
847
|
+
const record = { name, lib, via, file: at.file, line: at.line, status: path ? 'resolved' : refused || (SYSTEM_COPY.test(name) ? 'system' : 'missing'), path, ...(generated ? { generatedFrom: 'bms' } : {}) };
|
|
797
848
|
ctx.copies.push(record);
|
|
798
849
|
if (!path) return [];
|
|
799
850
|
if (stack.includes(path) || stack.length > 40) { record.status = 'recursive'; return []; }
|
|
800
851
|
if (++ctx.inclusions > MAX_INCLUSIONS || ctx.copyTokens > MAX_COPY_TOKENS) { record.status = 'expansion-limit'; return []; }
|
|
801
|
-
const src = ctx.readText(path);
|
|
852
|
+
const src = generated ? generated.text : ctx.readText(path);
|
|
802
853
|
// A copybook is read in the format in force where the COPY statement sits, which a >>SOURCE
|
|
803
854
|
// directive earlier in the including file may have changed from the file's starting format.
|
|
804
855
|
const fmt = detectFormat(src) === 'terminal' && ctx.copyFormat === 'auto' ? 'terminal' : (at.fmt || (ctx.copyFormat !== 'auto' ? ctx.copyFormat : (inheritedFormat || ctx.mainFormat)));
|
|
@@ -812,10 +863,13 @@ function includeCopy(name, lib, pairs, at, via, ctx, stack, inheritedFormat) {
|
|
|
812
863
|
const f = p.from.toks;
|
|
813
864
|
if (p.mode || !f.length) continue;
|
|
814
865
|
const pat = f.map(t => t.v).join('');
|
|
866
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- escaped
|
|
815
867
|
if (p.from.pseudo && /^[:(][A-Za-z0-9_-]*[:)]$/.test(pat)) { partial.push([p, new RegExp(escape(pat), 'gi')]); continue; }
|
|
816
868
|
if (p.from.pseudo || f.length !== 1 || f[0].t !== 'lit') continue;
|
|
817
869
|
const lit = `(['"])${escape(f[0].v)}\\1`;
|
|
870
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- lit is escaped
|
|
818
871
|
const touching = new RegExp(`${lit}(?=[A-Za-z0-9-])|(?<=[A-Za-z0-9-])${lit}`);
|
|
872
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- lit is escaped
|
|
819
873
|
if (norm.entries.some(e => touching.test(e.text))) partial.push([p, new RegExp(lit, 'g')]);
|
|
820
874
|
}
|
|
821
875
|
if (partial.length) {
|
|
@@ -832,6 +886,7 @@ function includeCopy(name, lib, pairs, at, via, ctx, stack, inheritedFormat) {
|
|
|
832
886
|
for (const [p] of partial) p.partial = true;
|
|
833
887
|
}
|
|
834
888
|
const { tokens, diags } = tokenize(norm, path);
|
|
889
|
+
if (generated) for (const t of tokens) t.line = generated.lines[t.line - 1] ?? t.line;
|
|
835
890
|
ctx.copyTokens += tokens.length;
|
|
836
891
|
ctx.diags.push(...diags, ...norm.diags.map(d => ({ ...d, file: path })));
|
|
837
892
|
const expanded = expand(tokens, ctx, [...stack, path], norm.finalFormat);
|
|
@@ -1855,7 +1910,7 @@ function collectCopybookDefines(src, format, ctx, depth) {
|
|
|
1855
1910
|
const sw = /(?:^|\s)>>\s*SOURCE\s+(?:FORMAT\s+)?(?:IS\s+)?(FREE|FIXED|VARIABLE|TERMINAL)/i.exec(l);
|
|
1856
1911
|
if (sw) { current = sw[1].toLowerCase(); continue; }
|
|
1857
1912
|
if (current !== 'free' && current !== 'terminal' && l.length > 6 && '*/'.includes(l[6])) continue;
|
|
1858
|
-
const code = l
|
|
1913
|
+
const code = withoutInlineComment(l);
|
|
1859
1914
|
const m = /(?:^|[\s.])COPY\s+("[^"]+"|'[^']+'|[A-Za-z0-9_-]+)/i.exec(code);
|
|
1860
1915
|
if (!m) continue;
|
|
1861
1916
|
const name = m[1].replace(/^["']|["']$/g, '');
|
package/lib/precompile-cics.mjs
CHANGED
|
@@ -122,12 +122,14 @@ export const constantsCopybook = (names) => `${names.map((n) => ` 01 ${n}
|
|
|
122
122
|
// prefix when TIOAPFX=YES, then for each named field its length, flag or attribute byte, extended
|
|
123
123
|
// attribute bytes and data, the output record redefining the input. Grouped (GRPNAME) and OCCURS
|
|
124
124
|
// fields are laid out differently, and a mapset holding either is not written. Returns the copybook
|
|
125
|
-
// text
|
|
125
|
+
// text, the names it declares and, for each of its lines, the BMS line it comes from; or null.
|
|
126
126
|
const ATTRIBUTE_LETTER = { COLOR: 'C', PS: 'P', HILIGHT: 'H', VALIDN: 'V', OUTLINE: 'U', SOSI: 'M', TRANSP: 'T' };
|
|
127
127
|
export function symbolicMapCopybook(mapset) {
|
|
128
128
|
const out = [];
|
|
129
129
|
const names = [];
|
|
130
|
-
const
|
|
130
|
+
const lines = [];
|
|
131
|
+
let from = mapset.line;
|
|
132
|
+
const line = (s) => { out.push(` ${s}`); lines.push(from); };
|
|
131
133
|
const declare = (n, s) => { names.push(n); line(s); };
|
|
132
134
|
// The assembler writes FILLER; each gets a name here, the layout unchanged, so the grade can tell
|
|
133
135
|
// the stand-in's items from the program's.
|
|
@@ -138,6 +140,7 @@ export function symbolicMapCopybook(mapset) {
|
|
|
138
140
|
if (fields.some((f) => f.grpname || f.occurs > 1)) return null;
|
|
139
141
|
const { input, output, attributes } = map.symbolic;
|
|
140
142
|
if (!input && !output) continue;
|
|
143
|
+
from = map.line;
|
|
141
144
|
const prefix = String(map.operands?.get?.('TIOAPFX') ?? mapset.options.tioapfx ?? '').toUpperCase() === 'YES';
|
|
142
145
|
const pic = (f, which) => f[which] || `X(${f.length})`;
|
|
143
146
|
if (input) {
|
|
@@ -145,6 +148,7 @@ export function symbolicMapCopybook(mapset) {
|
|
|
145
148
|
if (prefix) line(` 02 ${filler(input)} PIC X(12).`);
|
|
146
149
|
for (const f of fields) {
|
|
147
150
|
const n = f.name.toUpperCase();
|
|
151
|
+
from = f.line;
|
|
148
152
|
declare(`${n}L`, ` 02 ${n}L COMP PIC S9(4).`);
|
|
149
153
|
declare(`${n}F`, ` 02 ${n}F PIC X.`);
|
|
150
154
|
line(` 02 ${filler(input)} REDEFINES ${n}F.`);
|
|
@@ -154,10 +158,12 @@ export function symbolicMapCopybook(mapset) {
|
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
160
|
if (output) {
|
|
161
|
+
from = map.line;
|
|
157
162
|
declare(output, input ? `01 ${output} REDEFINES ${input}.` : `01 ${output}.`);
|
|
158
163
|
if (prefix) line(` 02 ${filler(output)} PIC X(12).`);
|
|
159
164
|
for (const f of fields) {
|
|
160
165
|
const n = f.name.toUpperCase();
|
|
166
|
+
from = f.line;
|
|
161
167
|
if (input) line(` 02 ${filler(output)} PIC X(3).`);
|
|
162
168
|
else { line(` 02 ${filler(output)} PIC X(2).`); declare(`${n}A`, ` 02 ${n}A PIC X.`); }
|
|
163
169
|
for (const a of attributes) declare(`${n}${ATTRIBUTE_LETTER[a]}`, ` 02 ${n}${ATTRIBUTE_LETTER[a]} PIC X.`);
|
|
@@ -165,5 +171,5 @@ export function symbolicMapCopybook(mapset) {
|
|
|
165
171
|
}
|
|
166
172
|
}
|
|
167
173
|
}
|
|
168
|
-
return out.length ? { text: `${out.join('\n')}\n`, names } : null;
|
|
174
|
+
return out.length ? { text: `${out.join('\n')}\n`, names, lines } : null;
|
|
169
175
|
}
|
package/lib/precompile.mjs
CHANGED
|
@@ -188,7 +188,7 @@ function place(lines, block, format, text) {
|
|
|
188
188
|
while (w < words.length && writeFrom + out.length + (out ? 1 : 0) + words[w].length <= to) out += (out ? ' ' : '') + words[w++];
|
|
189
189
|
const line = lines[li].padEnd(to);
|
|
190
190
|
lines[li] = line.slice(0, blankFrom) + ' '.repeat(Math.max(0, writeFrom - blankFrom)) + out.padEnd(Math.max(0, to - Math.max(writeFrom, blankFrom))) + line.slice(to);
|
|
191
|
-
if (li !== block.endLine) lines[li] = lines[li].
|
|
191
|
+
if (li !== block.endLine) lines[li] = lines[li].trimEnd();
|
|
192
192
|
}
|
|
193
193
|
return words.slice(w);
|
|
194
194
|
}
|
|
@@ -208,6 +208,7 @@ function spill(words, format) {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
// A division or section header, or a word, standing alone: not part of a longer name.
|
|
211
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- callers pass literal patterns
|
|
211
212
|
const word = (re) => new RegExp(`(?<![A-Z0-9-])${re}(?![A-Z0-9-])`, 'i');
|
|
212
213
|
const HEADERS = [['procedure', word('PROCEDURE\\s+DIVISION')], ['linkage', word('LINKAGE\\s+SECTION')], ['data', word('DATA\\s+DIVISION')], ['after', word('(?:REPORT|SCREEN)\\s+SECTION')]];
|
|
213
214
|
const PROGRAM_ID = word('PROGRAM-ID');
|
|
@@ -373,7 +374,7 @@ function translateText(t, shared, ctx, before = new Map(), after = new Map()) {
|
|
|
373
374
|
}
|
|
374
375
|
const rest = place(lines, b, format, text);
|
|
375
376
|
if (periodOnly) {
|
|
376
|
-
lines[b.endLine] = lines[b.endLine].
|
|
377
|
+
lines[b.endLine] = lines[b.endLine].trimEnd();
|
|
377
378
|
if (rest.length) { add(after, b.endLine, spill(rest, format)); state.spilled++; }
|
|
378
379
|
} else if (rest.length) {
|
|
379
380
|
// What followed END-EXEC - a period, or the rest of a sentence - follows the whole translation.
|
package/lib/revision.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"2144d560676dda84c8ae6546dfb7b858399f0b7d"}
|
package/lib/sarif.mjs
CHANGED
|
@@ -32,8 +32,9 @@ const LEVEL = { crit: 'error', high: 'error', med: 'warning', low: 'note', info:
|
|
|
32
32
|
// Locations are URI references: each segment is encoded, lone surrogates from Windows names replaced first.
|
|
33
33
|
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
34
34
|
const uriOf = (p) => String(p).split('/').map((seg) => encodeURIComponent(seg.replace(LONE_SURROGATE, '\uFFFD'))).join('/');
|
|
35
|
-
// Square brackets in a SARIF message are link syntax, and a finding's text can quote the tree.
|
|
36
|
-
|
|
35
|
+
// Square brackets in a SARIF message are link syntax, and a finding's text can quote the tree. A
|
|
36
|
+
// backslash escapes the character after it, so one before a bracket or a backslash is escaped too.
|
|
37
|
+
const textOf = (s) => (typeof s === 'string' ? s.replace(/\\(?=[\\[\]])|[[\]]/g, '\\$&') : s);
|
|
37
38
|
// SARIF's three levels fold critical into high and info into low, so the severity also travels as
|
|
38
39
|
// the number GitHub code scanning ranks by. Info is coverage and context, which rank as nothing.
|
|
39
40
|
const SECURITY_SEVERITY = { crit: '9.5', high: '8.0', med: '5.5', low: '3.0' };
|
package/lib/scan.mjs
CHANGED
|
@@ -74,7 +74,8 @@ function scanEach(root, opts) {
|
|
|
74
74
|
const reports = opts.repos.map(repo => ({ repo, r: scanAll(join(root, repo), { ...opts, repos: null, repoName: repo, feedRoot: opts.feedRoot || root }) }));
|
|
75
75
|
const findings = [];
|
|
76
76
|
const checked = [];
|
|
77
|
-
const fixedAt = (repo, x) => (x?.fixAt ? { exploitability: { ...x, fixAt: { ...x.fixAt, path: underRepo(repo, x.fixAt.path)
|
|
77
|
+
const fixedAt = (repo, x) => (x?.fixAt ? { exploitability: { ...x, fixAt: { ...x.fixAt, path: underRepo(repo, x.fixAt.path),
|
|
78
|
+
...(x.fixAt.builtInto ? { builtInto: { ...x.fixAt.builtInto, path: underRepo(repo, x.fixAt.builtInto.path) } } : {}) } } } : {});
|
|
78
79
|
const prefixed = (repo, f) => ({ ...f, path: underRepo(repo, f.path), ...(f.related ? { related: f.related.map(x => ({ ...x, path: underRepo(repo, x.path) })) } : {}), ...fixedAt(repo, f.exploitability) });
|
|
79
80
|
for (const { repo, r } of reports) {
|
|
80
81
|
for (const f of r.findings) findings.push(prefixed(repo, f));
|