@portll/cobolwork 0.4.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 +16 -0
- package/bin/cobolwork.mjs +7 -1
- package/lib/bms.mjs +8 -0
- package/lib/capabilities.mjs +2 -2
- package/lib/consequence.mjs +13 -0
- package/lib/constants.mjs +60 -0
- package/lib/control.mjs +61 -17
- package/lib/csd.mjs +6 -1
- package/lib/dataflow.mjs +58 -9
- package/lib/hlasm.mjs +79 -0
- package/lib/kernel/registry.mjs +4 -0
- package/lib/revision.json +1 -1
- package/lib/sets/abend.mjs +45 -7
- package/lib/sets/crypto.mjs +110 -0
- package/lib/sets/flow.mjs +18 -0
- package/lib/sets/hlasm.mjs +121 -0
- package/lib/sets/jcl.mjs +15 -1
- package/lib/sets/web.mjs +99 -4
- package/lib/sources.mjs +12 -2
- package/package.json +1 -1
- package/rules/compliance-cobit2019.json +132 -0
- package/rules/compliance-dora.json +143 -0
- package/rules/compliance-ffiec.json +143 -0
- package/rules/compliance-nist80053.json +143 -0
- package/rules/gitleaks-mainframe.toml +24 -10
- package/rules/hlasm-operations.json +451 -0
- package/rules/icsf-services.json +1178 -0
package/README.md
CHANGED
|
@@ -63,6 +63,13 @@ request and writes SARIF for code scanning: [docs/github-action.md](docs/github-
|
|
|
63
63
|
| JCL | credentials and security commands in in-stream data, destructive statements, `DLM=` tricks, FTP in cleartext or sending production data, production data touched by a test job |
|
|
64
64
|
| The source | names nothing declares (code that cannot compile), shadowed copybooks, payloads hidden in columns 73-80 or aimed at AI readers |
|
|
65
65
|
| The estate | production names outside production jobs, routable addresses, compiler and runtime versions with published advisories |
|
|
66
|
+
| Assembler | a switch to key zero or supervisor state, an instruction run through `EX`, cross-memory calls, the security product called directly, and the CSECT or ENTRY a COBOL `CALL` or a job step reaches |
|
|
67
|
+
| Cryptography | a single-length DES key, an MD5 or SHA-1 hash, or a fixed initialization vector asked of ICSF, read from IBM's parameter lists; an outbound CICS connection asking for HTTP |
|
|
68
|
+
|
|
69
|
+
HLASM is read, not assembled: its statements are read from the cards and its operations looked up
|
|
70
|
+
in a table that cites the IBM manual for each (`rules/hlasm-operations.json`). Macros are not
|
|
71
|
+
expanded and conditional assembly is not evaluated, so an operation a site macro issues is seen
|
|
72
|
+
where the macro is defined, and not where it is used.
|
|
66
73
|
|
|
67
74
|
[docs/rule-sets.md](docs/rule-sets.md) describes each in full, with what it deliberately leaves out.
|
|
68
75
|
Vendor packs for CA ACF2 and Top Secret, Control-M and Connect:Direct load only for estates that
|
|
@@ -107,6 +114,15 @@ that caller's storage. A value the subprogram keeps in its own storage can reach
|
|
|
107
114
|
Findings carry no source text, so a report can be stored and passed on without carrying the code
|
|
108
115
|
with it.
|
|
109
116
|
|
|
117
|
+
`trace` is one route, the shortest, and sources that reach one sink are merged into one finding,
|
|
118
|
+
counted in `sources`. With `--all-routes`, `scan` and `flow` give each path finding `routes`: every
|
|
119
|
+
source, and every statement on any route from one of them to the sink, by verb, file and line.
|
|
120
|
+
`complete` is false where the walk stopped at its budget or a step has no statement the engine can
|
|
121
|
+
place. It leaves out no statement on a route the engine follows, and may name one no route takes:
|
|
122
|
+
what a reader needs before saying a run covered every route (`docs/spec/reach.md` §9.8). Routes
|
|
123
|
+
through what the engine does not read, such as an unparsed program or a caller written in another
|
|
124
|
+
language, are not in it.
|
|
125
|
+
|
|
110
126
|
`fingerprint` is what the finding is, rather than where it is printed today: the rule, the program
|
|
111
127
|
and the paragraph or section it sits in (the job, step and DD for JCL), and the flagged statement's
|
|
112
128
|
own text. No line number goes into it, so code added above a finding does not change it. `diff`
|
package/bin/cobolwork.mjs
CHANGED
|
@@ -80,6 +80,7 @@ Options
|
|
|
80
80
|
--rules-path gitleaks print the path of the mainframe credential rules and exit
|
|
81
81
|
--quiet summary only
|
|
82
82
|
--full-trace list every hop of a data flow path, not its two ends
|
|
83
|
+
--all-routes give each data flow finding every statement on any route from its sources (scan, flow)
|
|
83
84
|
--base <ref> diff: the git revision to compare against
|
|
84
85
|
--head <ref> diff: the revision under review (default: the working tree)
|
|
85
86
|
--baseline <file> judgements to apply; without it, ${BASELINE_FILE} in the scanned tree
|
|
@@ -145,6 +146,7 @@ function parseArgs(argv) {
|
|
|
145
146
|
else if (a === '--rules-path') opts.rulesPath = argv[++i] || 'gitleaks';
|
|
146
147
|
else if (a === '--quiet') opts.quiet = true;
|
|
147
148
|
else if (a === '--full-trace') opts.fullTrace = true;
|
|
149
|
+
else if (a === '--all-routes') opts.allRoutes = true;
|
|
148
150
|
else if (a === '--base') opts.base = value();
|
|
149
151
|
else if (a === '--head') opts.head = value();
|
|
150
152
|
else if (a === '--baseline') opts.baseline = value();
|
|
@@ -250,6 +252,10 @@ if (opts.fullTrace && opts._.length && !TRACE_COMMANDS.includes(opts._[0])) {
|
|
|
250
252
|
`);
|
|
251
253
|
process.exit(2);
|
|
252
254
|
}
|
|
255
|
+
if (opts.allRoutes && opts._.length && !['scan', 'flow'].includes(opts._[0])) {
|
|
256
|
+
process.stderr.write(`cobolwork: --all-routes is for scan and flow\n`);
|
|
257
|
+
process.exit(2);
|
|
258
|
+
}
|
|
253
259
|
const ADVISORY_COMMANDS = ['scan', 'baseline', 'tui', 'explain', 'build'];
|
|
254
260
|
if (opts.advisoryFeeds && opts._.length && !ADVISORY_COMMANDS.includes(opts._[0])) {
|
|
255
261
|
process.stderr.write(`cobolwork: ${opts._[0]} reads no advisories; ${ADVISORY_COMMANDS.join(', ')} do\n`);
|
|
@@ -340,7 +346,7 @@ try {
|
|
|
340
346
|
} else if (command === 'evidence') {
|
|
341
347
|
process.exitCode = evidenceCommand(target, opts, { toolVersion: VERSION, write: (s) => process.stdout.write(s) });
|
|
342
348
|
} else if (command === 'scan' || command === 'flow') {
|
|
343
|
-
const flowOpts = { repos, fullTrace: opts.fullTrace === true, systemDirs };
|
|
349
|
+
const flowOpts = { repos, fullTrace: opts.fullTrace === true, allRoutes: opts.allRoutes === true, systemDirs };
|
|
344
350
|
// The site file sits beside the members and is not one of them, so it is named rather than found.
|
|
345
351
|
const site = resolve(root, SITE_FILE);
|
|
346
352
|
const pds = opts.pdsExport ? pdsExportTree(root, { systemDirs }) : null;
|
package/lib/bms.mjs
CHANGED
|
@@ -55,6 +55,13 @@ const sublist = (v) => {
|
|
|
55
55
|
return (inner ? splitOperands(inner[1]) : [v]).map((x) => x.trim()).filter(Boolean);
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
// L'NAME is NAME's length, and T', S', I', K', N', D' and O' its other attributes: the quote after the
|
|
59
|
+
// letter opens no string when the letter starts a term and a symbol follows. CL8'TEXT' and D'1.5'
|
|
60
|
+
// are constants, and their quotes do open one.
|
|
61
|
+
// https://www.ibm.com/docs/en/hla-and-tf/1.6.0?topic=instructions-data-attributes
|
|
62
|
+
const attributeReference = (text, i) => /[LTSIKNDO]/i.test(text[i - 1] || '')
|
|
63
|
+
&& !/[A-Z0-9$#@_]/i.test(text[i - 2] || '') && /[A-Z$#@_&]/i.test(text[i + 1] || '');
|
|
64
|
+
|
|
58
65
|
// Folds physical lines into statements. A map continues a statement in two ways, often both in one
|
|
59
66
|
// file. An operand that fills the line to column 71 carries on in column 16 of the next line, which
|
|
60
67
|
// is how a long INITIAL literal is split. A comma followed by a blank ends the line's operands and
|
|
@@ -78,6 +85,7 @@ export function foldStatements(src) {
|
|
|
78
85
|
for (let i = from; i < text.length; i++) {
|
|
79
86
|
const c = text[i];
|
|
80
87
|
if (o.quoted) { o.st.field += c; if (c === "'") o.quoted = false; continue; }
|
|
88
|
+
if (c === "'" && attributeReference(text, i)) { o.st.field += c; continue; }
|
|
81
89
|
if (c === "'") { o.quoted = true; o.st.field += c; continue; }
|
|
82
90
|
if (c === ' ' || c === '\t') {
|
|
83
91
|
o.reading = o.st.field.endsWith(',');
|
package/lib/capabilities.mjs
CHANGED
|
@@ -17,8 +17,8 @@ export const CAPABILITIES_SCHEMA_VERSION = 1;
|
|
|
17
17
|
|
|
18
18
|
// Each command, what it takes and what it writes.
|
|
19
19
|
export const COMMANDS = {
|
|
20
|
-
scan: { args: ['<path>'], options: ['--format', '--out', '--repos', '--only', '--quiet', '--full-trace', '--baseline', '--no-baseline', '--advisories', '--copylib', '--pds-export', '--evidence'], documents: ['cobolwork', 'sarif'] },
|
|
21
|
-
flow: { args: ['<path>'], options: ['--out', '--repos', '--quiet', '--full-trace', '--baseline', '--no-baseline', '--copylib', '--evidence'], documents: ['cobolwork-flow'] },
|
|
20
|
+
scan: { args: ['<path>'], options: ['--format', '--out', '--repos', '--only', '--quiet', '--full-trace', '--all-routes', '--baseline', '--no-baseline', '--advisories', '--copylib', '--pds-export', '--evidence'], documents: ['cobolwork', 'sarif'] },
|
|
21
|
+
flow: { args: ['<path>'], options: ['--out', '--repos', '--quiet', '--full-trace', '--all-routes', '--baseline', '--no-baseline', '--copylib', '--evidence'], documents: ['cobolwork-flow'] },
|
|
22
22
|
inventory: { args: ['<path>'], options: ['--out', '--quiet', '--copylib', '--evidence'], documents: ['cobolwork-inventory'] },
|
|
23
23
|
parse: { args: ['<file>'], options: ['--out'], documents: ['cobolwork-parse'] },
|
|
24
24
|
sbom: { args: ['<path>'], options: ['--name', '--out', '--quiet', '--copylib', '--evidence'], documents: ['cyclonedx'] },
|
package/lib/consequence.mjs
CHANGED
|
@@ -124,6 +124,19 @@ const RULE_CLASSES = {
|
|
|
124
124
|
'input-causes-abend': [],
|
|
125
125
|
'input-causes-abend-s0c4': [MUTATES],
|
|
126
126
|
'input-causes-abend-subscript-range': [MUTATES],
|
|
127
|
+
// Key zero or supervisor state is the operating system's authority, taken by the code itself.
|
|
128
|
+
'hlasm-supervisor-state-change': [ESCALATES],
|
|
129
|
+
// Nothing says input reaches EX's register, or which address space a cross-memory call reaches:
|
|
130
|
+
// the linkage tables grant that, and the source does not show them. Severity decides.
|
|
131
|
+
'hlasm-executes-built-instruction': [],
|
|
132
|
+
'hlasm-cross-memory-service': [],
|
|
133
|
+
// Disclosure in transit; severity decides.
|
|
134
|
+
'web-client-opens-cleartext': [],
|
|
135
|
+
'web-receive-length-exceeds-area': [MUTATES],
|
|
136
|
+
// Weak cryptography discloses or lets data be changed undetected; severity decides.
|
|
137
|
+
'icsf-single-length-des-key': [],
|
|
138
|
+
'icsf-weak-hash': [],
|
|
139
|
+
'icsf-fixed-initialization-vector': [],
|
|
127
140
|
};
|
|
128
141
|
|
|
129
142
|
// Coverage and context assert no defect, so they carry no consequence.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// The literals that can be in a data item when a statement reads it, from the program's text: its
|
|
3
|
+
// VALUE clause and its subordinates', each MOVE and STRING into it or into a group holding it, and a
|
|
4
|
+
// hop or two back through an item those name. Anything else that can write it - a computation, a
|
|
5
|
+
// READ or ACCEPT INTO, a CALL it is passed to by reference, a MOVE from a field with no constant -
|
|
6
|
+
// leaves it open, which a caller reads as "and something this cannot see".
|
|
7
|
+
const FIGURATIVE = /^(LOW-VALUES?|HIGH-VALUES?|SPACES?|ZEROS?|ZEROES|QUOTES?|NULLS?)$/;
|
|
8
|
+
const WRITERS = new Set(['COMPUTE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'READ', 'ACCEPT', 'UNSTRING', 'INITIALIZE', 'SET', 'RETURN', 'INSPECT']);
|
|
9
|
+
const NOT_OPERANDS = new Set(['TO', 'INTO', 'DELIMITED', 'BY', 'SIZE', 'OF', 'IN', 'ALL']);
|
|
10
|
+
|
|
11
|
+
// The item, its subordinates and the groups that hold it.
|
|
12
|
+
function family(p, name) {
|
|
13
|
+
const self = p.items.find((x) => x.name === name);
|
|
14
|
+
if (!self) return null;
|
|
15
|
+
const under = [];
|
|
16
|
+
const down = (it) => { for (const c of it.children || []) { if (c.level === 88) continue; under.push(c); down(c); } };
|
|
17
|
+
down(self);
|
|
18
|
+
const over = [];
|
|
19
|
+
for (let up = self.parent; up; up = up.parent) over.push(up);
|
|
20
|
+
return { self, under, over };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nameOf = (t) => (typeof t === 'string' ? t : t && t.u);
|
|
24
|
+
|
|
25
|
+
const valuesOf = (item) => (item.values || []).map((v) => (v.t === 'lit' ? String(v.v) : FIGURATIVE.test(v.u || '') ? v.u : null)).filter((v) => v !== null);
|
|
26
|
+
|
|
27
|
+
// `ignoreCall(call)` names calls that read the item and do not write it, such as the one being judged.
|
|
28
|
+
export function constantsReaching(p, name, { depth = 2, ignoreCall = () => false } = {}, seen = new Set()) {
|
|
29
|
+
const out = { values: [], open: false };
|
|
30
|
+
if (seen.has(name)) return out;
|
|
31
|
+
seen.add(name);
|
|
32
|
+
const f = family(p, name);
|
|
33
|
+
if (!f) return { values: [], open: true };
|
|
34
|
+
const names = new Set([name, ...f.under.map((x) => x.name), ...f.over.map((x) => x.name)]);
|
|
35
|
+
out.values.push(...valuesOf(f.self), ...f.under.flatMap(valuesOf));
|
|
36
|
+
for (const st of p.statements || []) {
|
|
37
|
+
if (!(st.targets || []).some((t) => names.has(nameOf(t)))) continue;
|
|
38
|
+
if (st.verb === 'MOVE' || st.verb === 'STRING') {
|
|
39
|
+
for (const t of st.sources || []) if (t && t.t === 'lit') out.values.push(String(t.v));
|
|
40
|
+
for (const l of st.literals || []) out.values.push(String(l && l.v !== undefined ? l.v : l));
|
|
41
|
+
for (const w of (st.sources || []).filter((t) => !(t && t.t === 'lit')).map(nameOf).filter((x) => x && !NOT_OPERANDS.has(x))) {
|
|
42
|
+
if (FIGURATIVE.test(w) || /^[+-]?\d/.test(w) || names.has(w)) { if (FIGURATIVE.test(w)) out.values.push(w); continue; }
|
|
43
|
+
if (depth === 0) { out.open = true; continue; }
|
|
44
|
+
const r = constantsReaching(p, w, { depth: depth - 1, ignoreCall }, seen);
|
|
45
|
+
out.values.push(...r.values);
|
|
46
|
+
if (r.open || !r.values.length) out.open = true;
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (WRITERS.has(st.verb)) out.open = true;
|
|
51
|
+
}
|
|
52
|
+
for (const c of p.calls || []) {
|
|
53
|
+
if (ignoreCall(c)) continue;
|
|
54
|
+
if ((c.using || []).some((u) => names.has(u.word) && u.mode !== 'CONTENT' && u.mode !== 'VALUE')) out.open = true;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A rule array is eight-byte keywords side by side; one held in several fields reads the same.
|
|
60
|
+
export const keywordsOf = (values) => new Set(values.flatMap((v) => (FIGURATIVE.test(v) ? [] : String(v).match(/.{1,8}/g) || []).map((k) => k.trim().toUpperCase()).filter(Boolean)));
|
package/lib/control.mjs
CHANGED
|
@@ -622,10 +622,14 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
622
622
|
|
|
623
623
|
// ---- checks ----
|
|
624
624
|
// The storage an item lies in: its record, or the record an 01 REDEFINES lays itself over.
|
|
625
|
+
const recordOf = new Map();
|
|
625
626
|
const itemsInRecord = (item) => {
|
|
626
|
-
let top = item;
|
|
627
|
+
let top = recordOf.get(item);
|
|
628
|
+
if (top) return top;
|
|
629
|
+
top = item;
|
|
627
630
|
while (top.parent) top = top.parent;
|
|
628
631
|
for (let hops = 0; top.redefinesItem && hops < 64; hops++) top = top.redefinesItem;
|
|
632
|
+
if (item.parent) recordOf.set(item, top);
|
|
629
633
|
return top;
|
|
630
634
|
};
|
|
631
635
|
const fieldKey = (f) => (f.index ? `IX:${f.index}` : f);
|
|
@@ -639,6 +643,33 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
639
643
|
const fEnd = f.offset + (f.contributes || f.size);
|
|
640
644
|
return w.offset < fEnd && f.offset < wEnd;
|
|
641
645
|
};
|
|
646
|
+
// Entries grouped by the storage their item lies in, so a lookup tries only what `overlaps` can
|
|
647
|
+
// accept: the same record, the same index, or a 66-level item, which overlaps any field.
|
|
648
|
+
const byStorage = (entries, itemOf) => {
|
|
649
|
+
const byRecord = new Map();
|
|
650
|
+
const byIndex = new Map();
|
|
651
|
+
const renames = [];
|
|
652
|
+
const plain = [];
|
|
653
|
+
const add = (map, k, e) => { const list = map.get(k); if (list) list.push(e); else map.set(k, [e]); };
|
|
654
|
+
for (const e of entries) {
|
|
655
|
+
const it = itemOf(e);
|
|
656
|
+
if (it.index) { add(byIndex, it.index, e); continue; }
|
|
657
|
+
plain.push(e);
|
|
658
|
+
if (it.level === 66) renames.push(e);
|
|
659
|
+
else add(byRecord, itemsInRecord(it), e);
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
mayOverlap: (it) => {
|
|
663
|
+
if (it.index) return byIndex.get(it.index) || [];
|
|
664
|
+
if (it.level === 66) return plain;
|
|
665
|
+
const same = byRecord.get(itemsInRecord(it));
|
|
666
|
+
return same ? (renames.length ? same.concat(renames) : same) : renames;
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
};
|
|
670
|
+
// Arguments the CICS API documents as data-values, which the command reads and never fills: the
|
|
671
|
+
// program or transaction it transfers to or starts, and the system it ships the request to.
|
|
672
|
+
const CICS_READS = { LINK: ['PROGRAM', 'TRANSID', 'SYSID'], XCTL: ['PROGRAM'], START: ['TRANSID', 'SYSID'], RETURN: ['TRANSID'], LOAD: ['PROGRAM'], RELEASE: ['PROGRAM'] };
|
|
642
673
|
const writtenBy = (n) => {
|
|
643
674
|
const out = [];
|
|
644
675
|
const add = (tok) => { const f = tok ? resolve(tok) : null; if (f) out.push(f.level === 88 && f.parent ? f.parent : f); };
|
|
@@ -667,28 +698,31 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
667
698
|
}
|
|
668
699
|
}
|
|
669
700
|
} else if (n.e) {
|
|
670
|
-
// A command may fill what any option is given directly, except what it sends FROM
|
|
671
|
-
// inside that argument's own parentheses - a subscript, a
|
|
701
|
+
// A command may fill what any option is given directly, except what it sends FROM and the
|
|
702
|
+
// data-values it reads. A name inside that argument's own parentheses - a subscript, a
|
|
703
|
+
// reference modification - is read.
|
|
704
|
+
const reads = (n.e.kind === 'CICS' && CICS_READS[n.e.toks[0]?.u]) || [];
|
|
672
705
|
let depth = 0;
|
|
673
706
|
let option = null;
|
|
674
707
|
for (let i = 0; i < n.e.toks.length; i++) {
|
|
675
708
|
const t = n.e.toks[i];
|
|
676
709
|
if (t.t === 'sep') { depth += t.v === '(' ? 1 : -1; continue; }
|
|
677
710
|
if (depth === 0) { option = t.t === 'word' ? t.u : option; continue; }
|
|
678
|
-
if (depth === 1 && t.t === 'word' && option !== 'FROM') add(t);
|
|
711
|
+
if (depth === 1 && t.t === 'word' && option !== 'FROM' && !reads.includes(option)) add(t);
|
|
679
712
|
}
|
|
680
713
|
}
|
|
681
714
|
return out;
|
|
682
715
|
};
|
|
683
716
|
// A VALUE holds for the whole run only if no statement writes storage that overlaps the item.
|
|
684
|
-
let
|
|
717
|
+
let written = null;
|
|
685
718
|
const neverWritten = (it) => {
|
|
686
719
|
if (it.external) return false;
|
|
687
|
-
if (!
|
|
688
|
-
|
|
689
|
-
for (const n of nodes) if (n.kind === 'stmt' || n.kind === 'exec' || n.kind === 'step' || n.kind === 'call')
|
|
720
|
+
if (!written) {
|
|
721
|
+
const all = [];
|
|
722
|
+
for (const n of nodes) if (n.kind === 'stmt' || n.kind === 'exec' || n.kind === 'step' || n.kind === 'call') all.push(...writtenBy(n));
|
|
723
|
+
written = byStorage(all, (w) => w);
|
|
690
724
|
}
|
|
691
|
-
return !
|
|
725
|
+
return !written.mayOverlap(it).some((w) => overlaps(w, it));
|
|
692
726
|
};
|
|
693
727
|
const constValue = (toks) => {
|
|
694
728
|
if (toks.length === 3 && WORD(toks[0], 'LENGTH') && WORD(toks[1], 'OF')) {
|
|
@@ -2373,17 +2407,17 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2373
2407
|
for (const f of [c.x, c.t, c.f, c.rt, c.rf]) if (f != null) list.push(f);
|
|
2374
2408
|
factsOfKey.set(c.key, list);
|
|
2375
2409
|
}
|
|
2410
|
+
const checkedFields = byStorage([...factsOfKey.keys()].filter((key) => fields.get(key)), (key) => fields.get(key));
|
|
2376
2411
|
const killCache = new Map();
|
|
2377
2412
|
const killOf = (w) => {
|
|
2378
2413
|
if (!w) return null;
|
|
2379
2414
|
let bits = killCache.get(w);
|
|
2380
2415
|
if (bits !== undefined) return bits;
|
|
2381
2416
|
bits = null;
|
|
2382
|
-
for (const
|
|
2383
|
-
|
|
2384
|
-
if (!f || !overlaps(w, f)) continue;
|
|
2417
|
+
for (const key of checkedFields.mayOverlap(w)) {
|
|
2418
|
+
if (!overlaps(w, fields.get(key))) continue;
|
|
2385
2419
|
bits ||= new Uint32Array(words);
|
|
2386
|
-
for (const x of
|
|
2420
|
+
for (const x of factsOfKey.get(key)) bits[x >>> 5] |= 1 << (x & 31);
|
|
2387
2421
|
}
|
|
2388
2422
|
killCache.set(w, bits);
|
|
2389
2423
|
return bits;
|
|
@@ -2629,10 +2663,15 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2629
2663
|
// Every route from `entry` that reaches `stopAt` or the program's end, meeting at joins. A call
|
|
2630
2664
|
// whose range never returns ends the route, as a STOP RUN does. `calls` collects the ranges the
|
|
2631
2665
|
// routes perform, for whoever needs to know what depends on what.
|
|
2666
|
+
// A run holds a bitset per node it reaches. Those of a run nobody reads any more are handed back
|
|
2667
|
+
// and reused, since allocating them afresh for every run was most of the analysis's collection time.
|
|
2668
|
+
const spare = [];
|
|
2669
|
+
const copyOf = (bits) => { const a = spare.pop(); if (!a) return bits.slice(); a.set(bits); return a; };
|
|
2670
|
+
const release = (IN) => { for (const a of IN.values()) spare.push(a); };
|
|
2632
2671
|
function run(entry, input, stopAt, calls = null) {
|
|
2633
2672
|
const IN = new Map();
|
|
2634
2673
|
let exitOut = null;
|
|
2635
|
-
IN.set(entry, input
|
|
2674
|
+
IN.set(entry, copyOf(input));
|
|
2636
2675
|
push(entry);
|
|
2637
2676
|
while (heap.length) {
|
|
2638
2677
|
const id = pop();
|
|
@@ -2643,7 +2682,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2643
2682
|
if (id === stopAt) { if (exitOut) meetInto(exitOut, out); else exitOut = out.slice(); continue; }
|
|
2644
2683
|
for (const s of n.succ) {
|
|
2645
2684
|
const cur = IN.get(s);
|
|
2646
|
-
if (!cur) { IN.set(s, out
|
|
2685
|
+
if (!cur) { IN.set(s, copyOf(out)); push(s); } else if (meetInto(cur, out)) push(s);
|
|
2647
2686
|
}
|
|
2648
2687
|
}
|
|
2649
2688
|
return { IN, exitOut };
|
|
@@ -2675,7 +2714,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2675
2714
|
for (const d of n.derive || []) if (d.needs.every((x) => has(bits, x))) approved.add(d);
|
|
2676
2715
|
for (const entry of n.implies || []) if (entry[2] && has(bits, entry[0])) approved.add(entry);
|
|
2677
2716
|
}
|
|
2678
|
-
|
|
2717
|
+
release(fromNone.IN);
|
|
2718
|
+
const fromAll = run(r.entry, ONES, r.end, calls);
|
|
2719
|
+
const top = fromAll.exitOut;
|
|
2720
|
+
release(fromAll.IN);
|
|
2679
2721
|
approved = null;
|
|
2680
2722
|
for (const k of calls) { if (!callers.has(k)) callers.set(k, new Set()); callers.get(k).add(r); }
|
|
2681
2723
|
const old = summaries.get(r.key);
|
|
@@ -2704,7 +2746,9 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2704
2746
|
waiting.delete(c.key);
|
|
2705
2747
|
const input = c.stopAt == null ? ZERO : rangeIn.get(c.key);
|
|
2706
2748
|
const res = run(c.entry, input, c.stopAt);
|
|
2749
|
+
const superseded = results.get(c.key);
|
|
2707
2750
|
results.set(c.key, res.IN);
|
|
2751
|
+
if (superseded) release(superseded);
|
|
2708
2752
|
for (const [id, bits] of res.IN) {
|
|
2709
2753
|
const n = nodes[id];
|
|
2710
2754
|
if (n.kind !== 'call' || !byKey.has(n.range.key)) continue;
|
|
@@ -2723,7 +2767,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
|
|
|
2723
2767
|
for (const IN of results.values()) {
|
|
2724
2768
|
for (const [id, bits] of IN) {
|
|
2725
2769
|
const had = facts.get(id);
|
|
2726
|
-
if (had) meetInto(had, bits); else facts.set(id, bits
|
|
2770
|
+
if (had) meetInto(had, bits); else facts.set(id, bits);
|
|
2727
2771
|
}
|
|
2728
2772
|
}
|
|
2729
2773
|
};
|
package/lib/csd.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const COMMAND = /^\s*(DEFINE|ADD|ALTER|APPEND|COPY|DELETE|INITIALIZE|LIST|REMOVE
|
|
|
14
14
|
// { tdqueues: Map(name -> { type, ddname, indirect, line }),
|
|
15
15
|
// transactions: Map(name -> { program, cmdsec, ressec, line }),
|
|
16
16
|
// tcpipservices: Map(name -> { port, protocol, ssl, authenticate, transaction, line }),
|
|
17
|
-
// urimaps: Map(name -> { usage, path, program, transaction, tcpipservice, line }) }
|
|
17
|
+
// urimaps: Map(name -> { usage, path, program, transaction, tcpipservice, scheme, host, attls, line }) }
|
|
18
18
|
export function parseCsd(text) {
|
|
19
19
|
const tdqueues = new Map();
|
|
20
20
|
const transactions = new Map();
|
|
@@ -64,6 +64,11 @@ export function parseCsd(text) {
|
|
|
64
64
|
program: attrs.get('PROGRAM') || null,
|
|
65
65
|
transaction: attrs.get('TRANSACTION') || null,
|
|
66
66
|
tcpipservice: attrs.get('TCPIPSERVICE') || null,
|
|
67
|
+
// A client URIMAP's scheme decides whether WEB OPEN URIMAP(name) asks for TLS; ATTLS(AWARE)
|
|
68
|
+
// has AT-TLS secure an HTTP scheme outside CICS.
|
|
69
|
+
scheme: attrs.get('SCHEME') || null,
|
|
70
|
+
host: attrs.get('HOST') || null,
|
|
71
|
+
attls: attrs.get('ATTLS') || null,
|
|
67
72
|
line: b.line,
|
|
68
73
|
});
|
|
69
74
|
} else if (kind === 'TDQUEUE') {
|
package/lib/dataflow.mjs
CHANGED
|
@@ -189,6 +189,9 @@ export function analyze(root, opts = {}) {
|
|
|
189
189
|
// Every hop of every trace, for the reader who is following one chain rather than counting
|
|
190
190
|
// findings. Off by default: a chain-shaped program turns this quadratic in the chain length.
|
|
191
191
|
const fullTrace = opts.fullTrace === true;
|
|
192
|
+
// Every statement on any route from a finding's source to its sink, not only the route the trace
|
|
193
|
+
// names: what a run must have executed before it can show a value does not arrive (reach.md §9.8).
|
|
194
|
+
const allRoutes = opts.allRoutes === true;
|
|
192
195
|
let held = 0;
|
|
193
196
|
|
|
194
197
|
// Which files, not just how many. A count names nothing to go and look at, and it cannot tell
|
|
@@ -216,7 +219,8 @@ export function analyze(root, opts = {}) {
|
|
|
216
219
|
const rel = (p) => { let r = relOf.get(p); if (r === undefined) { r = relPath(root, p); relOf.set(p, r); } return r; };
|
|
217
220
|
// `at` is the statement an edge that is not a statement's own is read at: the CALL or LINK that
|
|
218
221
|
// passes an argument, the PUT or GET of a container.
|
|
219
|
-
// `link` marks an edge into a callee at a call site ({ call }) or back out to it ({ ret })
|
|
222
|
+
// `link` marks an edge into a callee at a call site ({ call }) or back out to it ({ ret }), and
|
|
223
|
+
// `stmt` names the statement that carries an edge whose `why` is text.
|
|
220
224
|
const edge = (a, b, why, dir, at, link) => {
|
|
221
225
|
if (!a || !b || a === b) return;
|
|
222
226
|
a.edgesOut.push({ to: b, why, dir, ...(at != null ? { at } : {}), ...link });
|
|
@@ -797,7 +801,7 @@ export function analyze(root, opts = {}) {
|
|
|
797
801
|
if (verb === 'PUT' && o.has('CONTAINER')) {
|
|
798
802
|
const name = optionName(o, 'CONTAINER');
|
|
799
803
|
const from = nodeOfToken((o.get('FROM') || [])[0]);
|
|
800
|
-
if (name && from) edge(from, containerNode(name), `EXEC CICS PUT CONTAINER(${name})`, undefined, at.point);
|
|
804
|
+
if (name && from) edge(from, containerNode(name), `EXEC CICS PUT CONTAINER(${name})`, undefined, at.point, { stmt: { verb: 'EXEC CICS PUT', file: rel(at.file), line: at.line } });
|
|
801
805
|
if (name) channelOf.set(name, optionName(o, 'CHANNEL'));
|
|
802
806
|
}
|
|
803
807
|
// A service call sends every container on its channel, and its URI decides where they go.
|
|
@@ -811,7 +815,7 @@ export function analyze(root, opts = {}) {
|
|
|
811
815
|
if (verb === 'GET' && o.has('CONTAINER')) {
|
|
812
816
|
const name = optionName(o, 'CONTAINER');
|
|
813
817
|
const into = nodeOfToken((o.get('INTO') || o.get('SET') || [])[0]);
|
|
814
|
-
if (name && into) edge(containerNode(name), into, `EXEC CICS GET CONTAINER(${name})`, undefined, at.point);
|
|
818
|
+
if (name && into) edge(containerNode(name), into, `EXEC CICS GET CONTAINER(${name})`, undefined, at.point, { stmt: { verb: 'EXEC CICS GET', file: rel(at.file), line: at.line } });
|
|
815
819
|
}
|
|
816
820
|
// CONVERSE is always a client call; SEND is one only on a session this program opened. A
|
|
817
821
|
// plain WEB SEND answers the request that started the program, which is its job.
|
|
@@ -901,9 +905,9 @@ export function analyze(root, opts = {}) {
|
|
|
901
905
|
}
|
|
902
906
|
const commarea = (o.get('COMMAREA') || o.get('FROM') || [])[0];
|
|
903
907
|
const argNode = commarea ? nodeOfToken(commarea) : null;
|
|
904
|
-
if (pgm && pgm.t === 'lit' && argNode) transfers.push({ verb, callee: pgm.v.toUpperCase(), argNode, point: at.point });
|
|
908
|
+
if (pgm && pgm.t === 'lit' && argNode) transfers.push({ verb, callee: pgm.v.toUpperCase(), argNode, point: at.point, file: at.file, line: at.line });
|
|
905
909
|
else if (pgm && pgm.t === 'word' && argNode && o.has('PROGRAM')) {
|
|
906
|
-
for (const callee of programsNamed(pgm, e)) { transfers.push({ verb, callee, argNode, point: at.point, through: pgm.u }); transfersTo.add(callee); }
|
|
910
|
+
for (const callee of programsNamed(pgm, e)) { transfers.push({ verb, callee, argNode, point: at.point, file: at.file, line: at.line, through: pgm.u }); transfersTo.add(callee); }
|
|
907
911
|
}
|
|
908
912
|
if (pgm && pgm.t === 'lit') (o.has('TRANSID') && !o.has('PROGRAM') ? startsTransactions : transfersTo).add(String(pgm.v).trim().toUpperCase());
|
|
909
913
|
}
|
|
@@ -1046,22 +1050,24 @@ export function analyze(root, opts = {}) {
|
|
|
1046
1050
|
for (const t of p.transfers) {
|
|
1047
1051
|
const r = calleesOf(t.callee, p.file);
|
|
1048
1052
|
const site = ++callSites;
|
|
1053
|
+
const stmt = { verb: `EXEC CICS ${t.verb}`, file: rel(t.file), line: t.line };
|
|
1049
1054
|
for (const callee of r.list) {
|
|
1050
1055
|
if (!callee.commarea) continue;
|
|
1051
|
-
edge(t.argNode, callee.commarea, `EXEC CICS ${t.verb} COMMAREA to ${t.callee}${t.through ? `, the program ${t.through} names` : ''}${ofN(r, t.callee)}`, undefined, t.point, { call: site });
|
|
1052
|
-
if (t.verb === 'LINK') edge(callee.commarea, t.argNode, `COMMAREA returned from ${t.callee}${ofN(r, t.callee)}`, undefined, undefined, { ret: site });
|
|
1056
|
+
edge(t.argNode, callee.commarea, `EXEC CICS ${t.verb} COMMAREA to ${t.callee}${t.through ? `, the program ${t.through} names` : ''}${ofN(r, t.callee)}`, undefined, t.point, { call: site, stmt });
|
|
1057
|
+
if (t.verb === 'LINK') edge(callee.commarea, t.argNode, `COMMAREA returned from ${t.callee}${ofN(r, t.callee)}`, undefined, undefined, { ret: site, stmt });
|
|
1053
1058
|
}
|
|
1054
1059
|
}
|
|
1055
1060
|
for (const c of p.callSites) {
|
|
1056
1061
|
const r = calleesOf(c.name, p.file);
|
|
1057
1062
|
const site = ++callSites;
|
|
1063
|
+
const stmt = { verb: 'CALL', file: rel(c.file), line: c.line };
|
|
1058
1064
|
for (const callee of r.list) {
|
|
1059
1065
|
if (!callee.params) continue;
|
|
1060
1066
|
c.args.forEach((arg, i) => {
|
|
1061
1067
|
const param = callee.params[i];
|
|
1062
1068
|
if (!param || !arg.node || !param.node) return;
|
|
1063
|
-
edge(arg.node, param.node, `CALL '${c.display}' argument ${i + 1} at ${rel(c.file)}:${c.line}${ofN(r, c.display)}`, undefined, c.point, { call: site });
|
|
1064
|
-
if (arg.mode === 'REFERENCE' && param.mode === 'REFERENCE') edge(param.node, arg.node, `CALL '${c.display}' argument ${i + 1} written back${ofN(r, c.display)}`, undefined, undefined, { ret: site });
|
|
1069
|
+
edge(arg.node, param.node, `CALL '${c.display}' argument ${i + 1} at ${rel(c.file)}:${c.line}${ofN(r, c.display)}`, undefined, c.point, { call: site, stmt });
|
|
1070
|
+
if (arg.mode === 'REFERENCE' && param.mode === 'REFERENCE') edge(param.node, arg.node, `CALL '${c.display}' argument ${i + 1} written back${ofN(r, c.display)}`, undefined, undefined, { ret: site, stmt });
|
|
1065
1071
|
});
|
|
1066
1072
|
}
|
|
1067
1073
|
const callee = r.list.length === 1 ? r.list[0] : null;
|
|
@@ -1498,6 +1504,44 @@ export function analyze(root, opts = {}) {
|
|
|
1498
1504
|
}
|
|
1499
1505
|
return { partial, lo, hi, vague, ctx };
|
|
1500
1506
|
}
|
|
1507
|
+
// The statement that carries an edge: null for storage that overlays storage, which runs nothing,
|
|
1508
|
+
// and undefined where the engine does not know which statement it is.
|
|
1509
|
+
const OVERLAYS = new Set(['group', 'redefines', 'renames']);
|
|
1510
|
+
const statementOf = (e) => {
|
|
1511
|
+
if (e.stmt) return e.stmt;
|
|
1512
|
+
if (e.why && typeof e.why === 'object') return { verb: e.why.verb, file: e.why.file, line: e.why.line };
|
|
1513
|
+
return OVERLAYS.has(e.why) ? null : undefined;
|
|
1514
|
+
};
|
|
1515
|
+
// Every route from one walk's source to each sink it reached: the edges the walk could cross that
|
|
1516
|
+
// lead on to the sink, taken or not, by node rather than by call context or bytes. That lists a
|
|
1517
|
+
// statement no route needs sooner than it leaves out one a route does.
|
|
1518
|
+
function attachRoutes(awaiting, crossed, src, cut) {
|
|
1519
|
+
const into = new Map();
|
|
1520
|
+
for (const c of crossed) {
|
|
1521
|
+
const list = into.get(c.to);
|
|
1522
|
+
if (list) list.push(c); else into.set(c.to, [c]);
|
|
1523
|
+
}
|
|
1524
|
+
for (const { finding, at } of awaiting) {
|
|
1525
|
+
const onRoute = new Set([at]);
|
|
1526
|
+
const stack = [at];
|
|
1527
|
+
const statements = new Map();
|
|
1528
|
+
let unplaced = 0;
|
|
1529
|
+
while (stack.length) {
|
|
1530
|
+
for (const c of into.get(stack.pop()) || []) {
|
|
1531
|
+
const s = statementOf(c.e);
|
|
1532
|
+
if (s) statements.set(`${s.file}:${s.line}:${s.verb}`, s);
|
|
1533
|
+
else if (s === undefined) unplaced++;
|
|
1534
|
+
if (!onRoute.has(c.from)) { onRoute.add(c.from); stack.push(c.from); }
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
finding.routes = {
|
|
1538
|
+
sources: [{ kind: src.kind, file: rel(src.file), line: src.line }],
|
|
1539
|
+
statements: [...statements.values()].sort((a, b) => byText(a.file, b.file) || a.line - b.line || byText(a.verb, b.verb)),
|
|
1540
|
+
...(unplaced ? { unplaced } : {}),
|
|
1541
|
+
complete: !cut && !unplaced,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1501
1545
|
for (const start of nodes) {
|
|
1502
1546
|
if (!start.sources.length || !canReach[start.id]) continue;
|
|
1503
1547
|
for (const src of start.sources) {
|
|
@@ -1515,6 +1559,8 @@ export function analyze(root, opts = {}) {
|
|
|
1515
1559
|
// without a layout) partial taint does not descend at all.
|
|
1516
1560
|
const queue = [{ node: start, partial: false, lo: 0, hi: 0, vague: false, prev: null, why: 'source' }];
|
|
1517
1561
|
const partSeen = new Map();
|
|
1562
|
+
const crossedEdges = allRoutes ? [] : null;
|
|
1563
|
+
const awaiting = allRoutes ? [] : null;
|
|
1518
1564
|
stampWhole[start.id] = walk;
|
|
1519
1565
|
let cut = false;
|
|
1520
1566
|
const began = edgesWalked;
|
|
@@ -1563,6 +1609,7 @@ export function analyze(root, opts = {}) {
|
|
|
1563
1609
|
...(best.level ? { guard: { program: by.program, item: by.name, file: rel(best.check.file), line: best.check.line, ...(best.level === 2 ? { stops: true } : {}) } } : {}),
|
|
1564
1610
|
...(!best.level && missed ? { checkElsewhere: { program: missed.node.program, item: missed.node.name, file: rel(missed.check.file), line: missed.check.line } } : {}),
|
|
1565
1611
|
});
|
|
1612
|
+
if (awaiting) awaiting.push({ finding: findings[findings.length - 1], at: cur.id });
|
|
1566
1613
|
if (best.level && !own) credited.push({ finding: findings[findings.length - 1], node: cur, sink, level: best.level });
|
|
1567
1614
|
if (sink.group) useGroup.set(findings[findings.length - 1], `${src.kind}|${src.file}:${src.line}|${sink.kind}|${sink.group}`);
|
|
1568
1615
|
}
|
|
@@ -1572,6 +1619,7 @@ export function analyze(root, opts = {}) {
|
|
|
1572
1619
|
const to = e.to;
|
|
1573
1620
|
const crossed = cross(state, e);
|
|
1574
1621
|
if (!crossed) continue;
|
|
1622
|
+
if (crossedEdges) crossedEdges.push({ from: cur.id, to: to.id, e });
|
|
1575
1623
|
const { partial, lo, hi, vague, ctx } = crossed;
|
|
1576
1624
|
if (partial) {
|
|
1577
1625
|
const pk = `${to.id}|${ctx}`;
|
|
@@ -1583,6 +1631,7 @@ export function analyze(root, opts = {}) {
|
|
|
1583
1631
|
}
|
|
1584
1632
|
}
|
|
1585
1633
|
if (cut) stats.walksCut++;
|
|
1634
|
+
if (awaiting?.length) attachRoutes(awaiting, crossedEdges, src, cut);
|
|
1586
1635
|
// The walk keeps the shortest route to each sink, and credit on that route says nothing if
|
|
1587
1636
|
// another reaches the sink without it. So the walk runs again, refusing to let the value leave
|
|
1588
1637
|
// a node where the credit holds, and a sink it still reaches loses the credit: first for any
|
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/registry.mjs
CHANGED
|
@@ -28,6 +28,8 @@ 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
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';
|
|
31
33
|
import { toolName } from './ruleset.mjs';
|
|
32
34
|
|
|
33
35
|
// The order is the order a scan runs them and the order --only lists them. It is not significant
|
|
@@ -49,6 +51,8 @@ export const REGISTRY = [
|
|
|
49
51
|
{ name: 'semantics', scan: scanSemantics, rules: SEMANTICS_RULES },
|
|
50
52
|
{ name: 'zowe', scan: scanZowe, rules: ZOWE_RULES },
|
|
51
53
|
{ name: 'abend', scan: scanAbend, rules: ABEND_RULES },
|
|
54
|
+
{ name: 'hlasm', scan: scanHlasm, rules: HLASM_RULES },
|
|
55
|
+
{ name: 'crypto', scan: scanCrypto, rules: CRYPTO_RULES },
|
|
52
56
|
];
|
|
53
57
|
|
|
54
58
|
// Re-exported from the kernel's ruleset module, which is where it has to live: this file imports
|
package/lib/revision.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"2144d560676dda84c8ae6546dfb7b858399f0b7d"}
|