@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/sets/hlasm.mjs
CHANGED
|
@@ -6,16 +6,21 @@
|
|
|
6
6
|
//
|
|
7
7
|
// It also answers the question the COBOL side cannot: a CALL 'SUBRTN' with no COBOL program of that
|
|
8
8
|
// name may be an assembler module, and a CSECT or ENTRY of that name here is where it is.
|
|
9
|
-
import {
|
|
9
|
+
import { basename, extname } from 'node:path';
|
|
10
|
+
import { inScope, isAssembler, isJcl, isProgram, relPath, HLASM_EXT, COPY_EXT } from '../sources.mjs';
|
|
11
|
+
import { parseJcl } from '../jcl.mjs';
|
|
12
|
+
import { optableIn, processOptions } from '../hlasm/optable.mjs';
|
|
10
13
|
import { report } from '../kernel/ruleset.mjs';
|
|
11
14
|
import { treeFor, noteUnread } from '../kernel/source-tree.mjs';
|
|
12
15
|
import { eachWithinMemory } from '../kernel/memory.mjs';
|
|
13
|
-
import { readHlasm, OPERATIONS } from '../hlasm.mjs';
|
|
16
|
+
import { readHlasm, OPERATIONS, isDocument } from '../hlasm.mjs';
|
|
17
|
+
import { readHlasmStatements, parseHlasmStatement, NAMED_MACROS } from '../hlasm/read.mjs';
|
|
18
|
+
import { macroOperands, splitOperands } from '../hlasm/operands.mjs';
|
|
14
19
|
|
|
15
20
|
export const HLASM_RULES = {
|
|
16
21
|
'hlasm-supervisor-state-change': {
|
|
17
22
|
sev: 'crit', evidence: 'construct', cwe: 'CWE-250',
|
|
18
|
-
text: 'Assembler source switches to PSW key zero or supervisor state',
|
|
23
|
+
text: 'Assembler source switches to PSW key zero, another system key, or supervisor state',
|
|
19
24
|
impact: 'The code after the switch runs with the operating system\'s authority: it can read and change any storage, so a flaw in it is a flaw in the system rather than in one job',
|
|
20
25
|
remedy: 'Keep the authorized code to the instructions that need it, return to the caller\'s key and problem state straight after (MODESET KEY=NZERO, MODE=PROB), and APF-authorize the module only where it has to be',
|
|
21
26
|
},
|
|
@@ -31,6 +36,12 @@ export const HLASM_RULES = {
|
|
|
31
36
|
impact: 'A program call, a program transfer or a change of secondary address space moves execution or data access across address spaces, under the authority the linkage tables grant rather than the caller\'s own',
|
|
32
37
|
remedy: 'Confirm the entry tables and authorization indexes grant only the address spaces and keys the service needs, and that every PC routine validates what its caller passes',
|
|
33
38
|
},
|
|
39
|
+
'hlasm-runtime-module-name': {
|
|
40
|
+
sev: 'med', evidence: 'construct', cwe: 'CWE-470',
|
|
41
|
+
text: 'Assembler source loads or calls a module whose name it reads from storage at run time',
|
|
42
|
+
impact: 'EPLOC= and DE= give LINK, XCTL, LOAD and ATTACH the address of the name rather than the name, so whatever writes that storage chooses the program that runs, with the caller\'s authority',
|
|
43
|
+
remedy: 'Name the module with EP= where it is fixed; where it must vary, check the name against a list of the modules the program may run before the call',
|
|
44
|
+
},
|
|
34
45
|
'hlasm-calls-security-product': { sev: 'info', evidence: 'context', cwe: null, text: 'Assembler source calls the security product directly' },
|
|
35
46
|
'hlasm-provides-called-module': { sev: 'info', evidence: 'context', cwe: null, text: 'An assembler CSECT or ENTRY is a module a COBOL program in this tree CALLs' },
|
|
36
47
|
};
|
|
@@ -45,8 +56,17 @@ const STATE = { KEY: 'PSW key zero', EXTKEY: 'PSW key zero', MODE: 'supervisor s
|
|
|
45
56
|
|
|
46
57
|
const where = (use) => (use.inMacro ? ' It is in a macro definition, so it is in every program that uses the macro.' : '');
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
// EX and EXRL name their target instruction; when it is labelled in the same file, the finding says
|
|
60
|
+
// which instruction runs with the register's length or mask.
|
|
61
|
+
function executed(use, labels) {
|
|
62
|
+
if (use.class !== 'built-instruction') return '';
|
|
63
|
+
const target = (splitOperands(use.field || '')[1] || '').trim().toUpperCase();
|
|
64
|
+
const t = /^[A-Z$#@_][A-Z0-9$#@_]*$/.test(target) ? labels.get(target) : null;
|
|
65
|
+
return t ? ` runs ${t.operation}${t.field ? ` ${t.field}` : ''}` : '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function detail(use, labels) {
|
|
69
|
+
const written = `${use.name}${use.field ? ` ${use.field}` : ''}${executed(use, labels)}`;
|
|
50
70
|
if (use.class === 'security-product') {
|
|
51
71
|
const request = /\bREQUEST=([A-Z]+)/i.exec(use.field || '');
|
|
52
72
|
return `${request ? `${use.name} REQUEST=${request[1].toUpperCase()}` : use.name} calls the security product from assembler rather than through a service that checks on the program's behalf.${where(use)}`;
|
|
@@ -67,31 +87,122 @@ function cobolCalls(source) {
|
|
|
67
87
|
return calls;
|
|
68
88
|
}
|
|
69
89
|
|
|
90
|
+
// The repository's own definitions of macros the reader knows by name (IF, LINK, SAVE, ...): a
|
|
91
|
+
// member so named whose first statement is MACRO. Only those names are read, so the cost is a few
|
|
92
|
+
// files at most.
|
|
93
|
+
function repositoryMacros(tree, files) {
|
|
94
|
+
const found = new Set();
|
|
95
|
+
for (const f of files) {
|
|
96
|
+
const name = basename(f, extname(f)).toUpperCase();
|
|
97
|
+
if (!NAMED_MACROS.has(name) || found.has(name)) continue;
|
|
98
|
+
let text;
|
|
99
|
+
try { text = tree.text(f).text; } catch { continue; }
|
|
100
|
+
const first = readHlasmStatements(text).statements.find((s) => s.kind === 'statement');
|
|
101
|
+
if (first?.operation === 'MACRO') found.add(name);
|
|
102
|
+
}
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The OPTABLE the estate's assembly steps give in PARM (ASMA90, the old IEV90, or a procedure such as
|
|
107
|
+
// ASMACL), with where each was written. One level is the estate's; several leave each file to its own.
|
|
108
|
+
function jclOptables(root, tree, files) {
|
|
109
|
+
const levels = new Map();
|
|
110
|
+
for (const f of files) {
|
|
111
|
+
let job;
|
|
112
|
+
try { job = parseJcl(tree.text(f).text, f); } catch { continue; }
|
|
113
|
+
for (const step of job.steps) {
|
|
114
|
+
if (!/^(?:ASMA90|IEV90)$/i.test(step.pgm || '') && !/^(?:ASMA|HLASM|IEV)/i.test(step.proc || '')) continue;
|
|
115
|
+
for (const [k, v] of step.keywords) {
|
|
116
|
+
const found = /^PARM(?:\.|$)/.test(k) ? optableIn(v) : null;
|
|
117
|
+
if (found && !levels.has(found.level)) levels.set(found.level, `${relPath(root, f)}:${step.line}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return levels;
|
|
122
|
+
}
|
|
123
|
+
|
|
70
124
|
export function scanHlasm(root, opts = {}) {
|
|
71
125
|
const tree = treeFor(root, opts);
|
|
72
126
|
const all = tree.list().filter(inScope(opts));
|
|
73
127
|
const findings = [];
|
|
74
128
|
const stats = { filesScanned: 0, filesUnreadable: 0, assemblerFiles: 0, bmsFiles: 0, imsFiles: 0, unrecognisedFiles: 0, readNotAssembled: true };
|
|
129
|
+
const members = new Set(all.filter((f) => [...HLASM_EXT, ...COPY_EXT].some((e) => f.toLowerCase().endsWith(e))).map((f) => basename(f, extname(f)).toUpperCase()));
|
|
130
|
+
const libraryMacros = repositoryMacros(tree, all.filter(isAssembler));
|
|
131
|
+
const forms = { mvs38: opts.mvs38Forms !== false };
|
|
132
|
+
const fromJcl = opts.hlasmOptable ? new Map() : jclOptables(root, tree, all.filter(isJcl));
|
|
133
|
+
const estateOptable = opts.hlasmOptable ?? (fromJcl.size === 1 ? [...fromJcl.keys()][0] : null);
|
|
134
|
+
if (opts.hlasmOptable) stats.hlasmOptable = { level: opts.hlasmOptable, from: 'option' };
|
|
135
|
+
else if (fromJcl.size === 1) stats.hlasmOptable = { level: estateOptable, from: [...fromJcl.values()][0] };
|
|
136
|
+
else if (fromJcl.size > 1) stats.hlasmOptableConflict = Object.fromEntries(fromJcl);
|
|
137
|
+
// The table a file is assembled with: the option given to the scan, then a *PROCESS OVERRIDE in
|
|
138
|
+
// the file, the estate's PARM, and the file's own *PROCESS, as HLASM ranks them.
|
|
139
|
+
const optableOf = (src) => {
|
|
140
|
+
if (opts.hlasmOptable) return opts.hlasmOptable;
|
|
141
|
+
const own = optableIn(processOptions(src));
|
|
142
|
+
return own?.override ? own.level : estateOptable ?? own?.level ?? null;
|
|
143
|
+
};
|
|
144
|
+
const refused = {};
|
|
145
|
+
const macroCalls = new Map();
|
|
146
|
+
const copyNotFound = new Set();
|
|
147
|
+
// Reads a file's statements: its labels, what the reader refused, and the macros and COPY members it
|
|
148
|
+
// uses, which count toward the coverage report unless the file is a document.
|
|
149
|
+
function readStatements(src, path) {
|
|
150
|
+
const labels = new Map();
|
|
151
|
+
const read = readHlasmStatements(src, { libraryMacros }).statements;
|
|
152
|
+
const statements = read.filter((st) => !st.inMacro);
|
|
153
|
+
const fileForms = { ...forms, optable: optableOf(src) };
|
|
154
|
+
const results = statements.map((st) => parseHlasmStatement(st, fileForms));
|
|
155
|
+
const refusals = results.filter((res) => res.status === 'unparsed' || res.status === 'unknown').length;
|
|
156
|
+
if (isDocument(read.filter((st) => st.kind === 'statement'), statements.length, refusals)) return null;
|
|
157
|
+
statements.forEach((st, i) => {
|
|
158
|
+
const res = results[i];
|
|
159
|
+
if (st.kind === 'statement' && st.name && !st.name.startsWith('&') && !st.name.startsWith('.')) labels.set(st.name.toUpperCase(), st);
|
|
160
|
+
if (res.node?.mvs38) stats.mvs38Forms = (stats.mvs38Forms || 0) + 1;
|
|
161
|
+
if (res.status === 'unparsed' || res.status === 'unknown') (refused[res.kind] ||= { count: 0, first: `${path}:${st.line}` }).count++;
|
|
162
|
+
const name = res.kind === 'MACRO CALL' ? st.operation : NAMED_MACROS.has(res.kind) ? res.kind : null;
|
|
163
|
+
if (name) {
|
|
164
|
+
const m = macroCalls.get(name) || { name, calls: 0, definedInTree: members.has(name) };
|
|
165
|
+
m.calls++;
|
|
166
|
+
macroCalls.set(name, m);
|
|
167
|
+
}
|
|
168
|
+
if (res.kind === 'COPY') {
|
|
169
|
+
const member = String(res.status === 'parsed' ? res.node.member : (/^\s*([A-Z$#@][A-Z0-9$#@_]*)/i.exec(st.field || '') || [])[1] || '').toUpperCase();
|
|
170
|
+
if (member && !members.has(member)) copyNotFound.add(member);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return labels;
|
|
174
|
+
}
|
|
75
175
|
const defines = new Map();
|
|
76
176
|
|
|
77
177
|
const run = eachWithinMemory(all.filter(isAssembler), (f) => {
|
|
78
178
|
let src;
|
|
79
179
|
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
80
180
|
stats.filesScanned++;
|
|
81
|
-
const r = readHlasm(src);
|
|
181
|
+
const r = readHlasm(src, forms);
|
|
82
182
|
if (r.kind === 'bms') { stats.bmsFiles++; return src.length; }
|
|
83
183
|
if (r.kind === 'ims') { stats.imsFiles++; return src.length; }
|
|
84
184
|
if (r.kind === 'unrecognised') { stats.unrecognisedFiles++; return src.length; }
|
|
85
|
-
stats.
|
|
185
|
+
if (r.kind === 'listing') { stats.listingFiles = (stats.listingFiles || 0) + 1; return src.length; }
|
|
86
186
|
const path = relPath(root, f);
|
|
187
|
+
const labels = readStatements(src, path);
|
|
188
|
+
if (!labels) { stats.documentFiles = (stats.documentFiles || 0) + 1; return src.length; }
|
|
189
|
+
stats.assemblerFiles++;
|
|
87
190
|
for (const d of r.defines) if (!defines.has(d.name)) defines.set(d.name, { ...d, path });
|
|
88
191
|
for (const use of r.operations) {
|
|
192
|
+
if (use.class === 'module-load') {
|
|
193
|
+
const { keywords } = macroOperands(use.field || '');
|
|
194
|
+
const at = keywords.has('EPLOC') ? 'EPLOC' : keywords.has('DE') ? 'DE' : null;
|
|
195
|
+
if (at) findings.push({ rule: 'hlasm-runtime-module-name', path, line: use.line, detail: `${use.name} ${at}=${keywords.get(at)} names the module by the address of its name, so the program that runs is whatever name that storage holds.${where(use)}` });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
89
198
|
if (use.name === 'MODESET') {
|
|
90
199
|
if (use.stateChange) findings.push({ rule: 'hlasm-supervisor-state-change', path, line: use.line, detail: `MODESET ${use.stateChange} switches to ${STATE[use.stateChange.split('=')[0]]}.${where(use)}` });
|
|
200
|
+
// A system key writes storage problem-state keys cannot, short of key zero's reach.
|
|
201
|
+
else if (use.systemKey) findings.push({ rule: 'hlasm-supervisor-state-change', sev: 'high', path, line: use.line, detail: `MODESET ${use.systemKey.written} switches to PSW key ${use.systemKey.key}, a system key.${where(use)}` });
|
|
91
202
|
continue;
|
|
92
203
|
}
|
|
93
204
|
const rule = RULE_OF_CLASS[use.class];
|
|
94
|
-
if (rule) findings.push({ rule, path, line: use.line, detail: detail(use) });
|
|
205
|
+
if (rule) findings.push({ rule, path, line: use.line, detail: detail(use, labels) });
|
|
95
206
|
}
|
|
96
207
|
return src.length;
|
|
97
208
|
}, { label: 'hlasm', maxBytes: opts.maxSourceBytes ?? Infinity });
|
|
@@ -117,5 +228,10 @@ export function scanHlasm(root, opts = {}) {
|
|
|
117
228
|
}
|
|
118
229
|
}
|
|
119
230
|
|
|
231
|
+
// Each statement the reader refused, each macro whose expansion was not read and each COPY member
|
|
232
|
+
// the tree does not hold is named: an operation inside any of them is one this set did not see.
|
|
233
|
+
if (Object.keys(refused).length) { stats.statementsNotRead = refused; stats.coverageIncomplete = true; }
|
|
234
|
+
if (copyNotFound.size) { stats.copyNotFound = [...copyNotFound].sort(); stats.coverageIncomplete = true; }
|
|
235
|
+
if (macroCalls.size) stats.macrosNotExpanded = [...macroCalls.values()].sort((a, b) => b.calls - a.calls || a.name.localeCompare(b.name)).slice(0, 50);
|
|
120
236
|
return report('hlasm', { rules: HLASM_RULES, findings, stats, run });
|
|
121
237
|
}
|
package/lib/sets/ims.mjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// IMS database and program definitions: what a PSB lets a program change, SENSEGs that name segments
|
|
3
|
+
// their DBD does not define, and what DBDGEN, PSBGEN or ACBGEN refuses (lib/ims/rules.mjs). DBDs and
|
|
4
|
+
// PSBs are assembler macros and are often named .asm, so assembler files are read too, and kept only
|
|
5
|
+
// where they hold a DBD or PSB.
|
|
6
|
+
import { inScope, isAssembler, isIms, isJcl, isProgram, relPath } from '../sources.mjs';
|
|
7
|
+
import { report } from '../kernel/ruleset.mjs';
|
|
8
|
+
import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
|
|
9
|
+
import { eachWithinMemory } from '../kernel/memory.mjs';
|
|
10
|
+
import { modelOf } from '../ims/model.mjs';
|
|
11
|
+
import { IMS_RULES, checkModels, checkProcoptAgainstCalls } from '../ims/rules.mjs';
|
|
12
|
+
import { DLI_ROUTINE, DLI_FUNCTIONS, DLI_SERVICES, psbOfParm } from '../ims/dli.mjs';
|
|
13
|
+
import { parseJcl } from '../jcl.mjs';
|
|
14
|
+
import { constantsReaching } from '../constants.mjs';
|
|
15
|
+
import { execReading } from '../exec-reading.mjs';
|
|
16
|
+
|
|
17
|
+
export { IMS_RULES };
|
|
18
|
+
|
|
19
|
+
export function scanIms(root, opts = {}) {
|
|
20
|
+
const tree = treeFor(root, opts);
|
|
21
|
+
const files = tree.list().filter(inScope(opts)).filter((f) => isIms(f) || isAssembler(f));
|
|
22
|
+
const stats = { filesScanned: 0, filesUnreadable: 0, dbdFiles: 0, psbFiles: 0 };
|
|
23
|
+
const models = [];
|
|
24
|
+
const unread = {};
|
|
25
|
+
const run = eachWithinMemory(files, (f) => {
|
|
26
|
+
let src;
|
|
27
|
+
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
28
|
+
const model = modelOf(src);
|
|
29
|
+
if (!model) return src.length;
|
|
30
|
+
stats.filesScanned++;
|
|
31
|
+
stats[model.kind === 'DBD' ? 'dbdFiles' : 'psbFiles']++;
|
|
32
|
+
const path = relPath(root, f);
|
|
33
|
+
for (const u of model.unread) (unread[u.operation || 'UNREADABLE'] ||= { count: 0, first: `${path}:${u.line}` }).count++;
|
|
34
|
+
models.push({ path, model });
|
|
35
|
+
return src.length;
|
|
36
|
+
}, { label: 'ims', maxBytes: opts.maxSourceBytes ?? Infinity });
|
|
37
|
+
|
|
38
|
+
// A statement the reader refused may hold a segment, a field or a PCB the rules never saw.
|
|
39
|
+
if (Object.keys(unread).length) { stats.statementsNotRead = unread; stats.coverageIncomplete = true; }
|
|
40
|
+
|
|
41
|
+
// Programs are read only when a PSB could be named for one, and only those that issue DL/I.
|
|
42
|
+
const programs = [];
|
|
43
|
+
const links = [];
|
|
44
|
+
let calls = null;
|
|
45
|
+
if (models.some(({ model }) => model.kind === 'PSB')) {
|
|
46
|
+
const sources = tree.list().filter(inScope(opts)).filter((f) => isProgram(f) || isJcl(f));
|
|
47
|
+
calls = eachWithinMemory(sources, (f) => {
|
|
48
|
+
let src;
|
|
49
|
+
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
50
|
+
const path = relPath(root, f);
|
|
51
|
+
if (isJcl(f)) {
|
|
52
|
+
if (!/DFSRRC00/i.test(src)) return src.length;
|
|
53
|
+
let jcl;
|
|
54
|
+
try { jcl = parseJcl(src); } catch { return src.length; }
|
|
55
|
+
for (const step of jcl.steps || []) {
|
|
56
|
+
const l = String(step.pgm || '').toUpperCase() === 'DFSRRC00' ? psbOfParm(step.parm) : null;
|
|
57
|
+
if (l) links.push({ ...l, at: `${path}:${step.line}` });
|
|
58
|
+
}
|
|
59
|
+
return src.length;
|
|
60
|
+
}
|
|
61
|
+
if (!/CBLTDLI|AIBTDLI|EXEC\s+DLI/i.test(src)) return src.length;
|
|
62
|
+
let r;
|
|
63
|
+
try { r = tree.parse(f, src); } catch (e) { noteUnparsed(stats, tree, f, e); return src.length; }
|
|
64
|
+
for (const prog of r.programs) {
|
|
65
|
+
const read = dliCallsOf(prog);
|
|
66
|
+
const id = String(prog.id).toUpperCase();
|
|
67
|
+
programs.push({ id, path, line: prog.line || 1, functions: read.functions });
|
|
68
|
+
for (const psb of read.schedules) links.push({ program: id, psb, at: `${path}, its own scheduling call` });
|
|
69
|
+
}
|
|
70
|
+
return src.length;
|
|
71
|
+
}, { label: 'ims programs', maxBytes: opts.maxSourceBytes ?? Infinity });
|
|
72
|
+
stats.programsWithDli = programs.length;
|
|
73
|
+
}
|
|
74
|
+
const findings = [...checkModels(models), ...checkProcoptAgainstCalls(models, programs, links)];
|
|
75
|
+
return report('ims', { rules: IMS_RULES, findings, stats, run: calls ? bothRuns(run, calls) : run });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The DL/I database functions a program issues itself, null when they cannot all be told: a function
|
|
79
|
+
// code no constant decides, a CALL that passes on a PCB or a LINKAGE item, which may hold one, or a
|
|
80
|
+
// CICS LINK or XCTL from a program that schedules a PSB, whose target can issue DL/I against it. And
|
|
81
|
+
// the PSBs it schedules itself, by EXEC DLI SCHD or a PCB call, where a constant names one.
|
|
82
|
+
function dliCallsOf(prog) {
|
|
83
|
+
let functions = new Set();
|
|
84
|
+
const schedules = new Set();
|
|
85
|
+
const ignore = { ignoreCall: (x) => DLI_ROUTINE.test(String(x.name)) };
|
|
86
|
+
const valuesOf = (a) => {
|
|
87
|
+
if (!a) return { values: [], open: true };
|
|
88
|
+
if (a.lit != null) return { values: [String(a.lit)], open: false };
|
|
89
|
+
return a.word ? constantsReaching(prog, a.word, ignore) : { values: [], open: true };
|
|
90
|
+
};
|
|
91
|
+
const named = (k) => (k.open ? null : k.values.map((v) => String(v).trim().toUpperCase()));
|
|
92
|
+
const known = (v) => Object.hasOwn(DLI_FUNCTIONS, v) || DLI_SERVICES.has(v);
|
|
93
|
+
const byName = new Map((prog.items || []).map((it) => [it.name, it]));
|
|
94
|
+
const topOf = (it) => { while (it && it.parent) it = it.parent; return it; };
|
|
95
|
+
// The function code is the first of the two arguments whose constants name one; the PCB or AIB follows it.
|
|
96
|
+
const functionAt = (using) => [0, 1].find((i) => valuesOf(using[i]).values.some((v) => known(String(v).trim().toUpperCase())));
|
|
97
|
+
const pcbs = new Set();
|
|
98
|
+
for (const c of prog.calls || []) {
|
|
99
|
+
if (c.kind !== 'L' || !DLI_ROUTINE.test(String(c.name))) continue;
|
|
100
|
+
const at = functionAt(c.using || []);
|
|
101
|
+
const pcb = at === undefined ? null : (c.using || [])[at + 1];
|
|
102
|
+
if (pcb && pcb.word) pcbs.add(pcb.word);
|
|
103
|
+
}
|
|
104
|
+
const passesOn = (a) => a.word && (pcbs.has(a.word) || (topOf(byName.get(a.word)) || {}).section === 'LINKAGE');
|
|
105
|
+
for (const c of prog.calls || []) {
|
|
106
|
+
if (c.kind !== 'L') { functions = null; continue; }
|
|
107
|
+
if (!DLI_ROUTINE.test(String(c.name))) {
|
|
108
|
+
if ((c.using || []).some(passesOn)) functions = null;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const using = c.using || [];
|
|
112
|
+
const at = functionAt(using);
|
|
113
|
+
const codes = at === undefined ? null : named(valuesOf(using[at]));
|
|
114
|
+
if (!codes || !codes.every(known)) { functions = null; continue; }
|
|
115
|
+
if (codes.includes('PCB')) for (const n of named(valuesOf(using[at + 1])) || []) schedules.add(n);
|
|
116
|
+
if (functions) for (const fn of codes) if (Object.hasOwn(DLI_FUNCTIONS, fn)) functions.add(fn);
|
|
117
|
+
}
|
|
118
|
+
for (const e of prog.execs || []) {
|
|
119
|
+
if (e.kind !== 'DLI') continue;
|
|
120
|
+
const { verb, opts } = execReading(e);
|
|
121
|
+
if (verb === 'SCHD') {
|
|
122
|
+
const tok = (opts.get('PSB') || []).find((t) => t.t === 'word' || t.t === 'lit');
|
|
123
|
+
const k = !tok ? { values: [], open: true } : tok.t === 'lit' ? { values: [tok.v], open: false } : constantsReaching(prog, tok.v, ignore);
|
|
124
|
+
for (const n of named(k) || []) schedules.add(n);
|
|
125
|
+
} else if (functions && Object.hasOwn(DLI_FUNCTIONS, verb)) functions.add(verb);
|
|
126
|
+
}
|
|
127
|
+
const transfers = (prog.execs || []).some((e) => e.kind === 'CICS' && ['LINK', 'XCTL'].includes(execReading(e).verb));
|
|
128
|
+
if (schedules.size && transfers) functions = null;
|
|
129
|
+
return { functions: functions && functions.size ? functions : null, schedules: [...schedules] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function bothRuns(a, b) {
|
|
133
|
+
return {
|
|
134
|
+
skipped: [...a.skipped, ...b.skipped],
|
|
135
|
+
stoppedBy: a.stoppedBy || b.stoppedBy,
|
|
136
|
+
peakHeapBytes: Math.max(a.peakHeapBytes || 0, b.peakHeapBytes || 0) || undefined,
|
|
137
|
+
note: [a.note, b.note].filter(Boolean).join(' ') || undefined,
|
|
138
|
+
};
|
|
139
|
+
}
|
package/lib/sets/log.mjs
CHANGED
|
@@ -23,7 +23,8 @@ import { parseJcl } from '../jcl.mjs';
|
|
|
23
23
|
import { compileStepOptions } from '../options.mjs';
|
|
24
24
|
import { report } from '../kernel/ruleset.mjs';
|
|
25
25
|
import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
|
|
26
|
-
import {
|
|
26
|
+
import { drive, loopOver } from '../kernel/shared-pass.mjs';
|
|
27
|
+
import { execReading } from '../exec-reading.mjs';
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
export const LOG_RULES = {
|
|
@@ -107,17 +108,16 @@ function logSinks(p, where, path) {
|
|
|
107
108
|
}
|
|
108
109
|
for (const e of p.execs || []) {
|
|
109
110
|
if (e.kind !== 'CICS') continue;
|
|
110
|
-
const
|
|
111
|
-
const isTd =
|
|
112
|
-
const isJournal =
|
|
111
|
+
const { verb, sub, words, opts } = execReading(e);
|
|
112
|
+
const isTd = verb === 'WRITEQ' && sub === 'TD';
|
|
113
|
+
const isJournal = verb === 'WRITE' && words.includes('JOURNALNAME');
|
|
113
114
|
// The console, read by whoever is watching the region. A credential on it is as exposed as one
|
|
114
115
|
// in SYSOUT, and it outlives the screen in the system log.
|
|
115
|
-
const isOperator =
|
|
116
|
+
const isOperator = verb === 'WRITE' && sub === 'OPERATOR';
|
|
116
117
|
if (!isTd && !isJournal && !isOperator) continue;
|
|
117
118
|
// FROM names the area written, TEXT names the console message; that name is what this reads.
|
|
118
119
|
const opt = isOperator ? 'TEXT' : 'FROM';
|
|
119
|
-
const
|
|
120
|
-
const arg = i >= 0 ? e.toks.slice(i + 1).find((t) => t.t === 'word') : null;
|
|
120
|
+
const arg = (opts.get(opt) || []).find((t) => t.t === 'word') || null;
|
|
121
121
|
const kind = isTd ? 'WRITEQ TD' : isOperator ? 'WRITE OPERATOR' : 'WRITE JOURNALNAME';
|
|
122
122
|
if (arg) out.push({ kind, name: arg.u, ...where(e, path) });
|
|
123
123
|
}
|
|
@@ -337,7 +337,7 @@ const memberOf = (file) => basename(String(file || ''), extname(String(file || '
|
|
|
337
337
|
const runByJob = (batch, p, file) => batch.unsure
|
|
338
338
|
|| [String(p.id || '').toUpperCase(), memberOf(file)].some((n) => n && (batch.runs.has(n) || batch.runs.has(n.slice(0, 8))));
|
|
339
339
|
|
|
340
|
-
export function
|
|
340
|
+
export function* scanLogSteps(root, opts = {}) {
|
|
341
341
|
const tree = treeFor(root, opts);
|
|
342
342
|
const files = tree.list().filter(isProgram).filter(inScope(opts));
|
|
343
343
|
const findings = [];
|
|
@@ -364,7 +364,7 @@ export function scanLog(root, opts = {}) {
|
|
|
364
364
|
return { rule: 'display-echoes-a-credential', detail: `${p.id} shows ${w.name} on the terminal to the person who typed it: not a log, but the screen and its scrollback keep it` };
|
|
365
365
|
};
|
|
366
366
|
|
|
367
|
-
const run =
|
|
367
|
+
const run = yield loopOver(files, (f) => {
|
|
368
368
|
let src;
|
|
369
369
|
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
370
370
|
// Every EXEC CICS WRITE form, not the two this set started with. WRITE OPERATOR was added as a
|
|
@@ -404,3 +404,5 @@ export function scanLog(root, opts = {}) {
|
|
|
404
404
|
|
|
405
405
|
return report('log', { rules: LOG_RULES, findings, stats, run });
|
|
406
406
|
}
|
|
407
|
+
|
|
408
|
+
export const scanLog = (root, opts = {}) => drive(scanLogSteps(root, opts));
|
package/lib/sets/opaque.mjs
CHANGED
|
@@ -74,21 +74,41 @@ export function scanOpaque(root, opts = {}) {
|
|
|
74
74
|
const path = relPath(root, f);
|
|
75
75
|
|
|
76
76
|
// Which names were declared as pointers, so a CALL through one can be told from an ordinary
|
|
77
|
-
// CALL of a variable - which the flow rules already report as a dynamic program load.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
// CALL of a variable - which the flow rules already report as a dynamic program load. Found
|
|
78
|
+
// only once a CALL needs it, since the search tries every word of the program.
|
|
79
|
+
let pointers = null;
|
|
80
|
+
const pointerNames = () => {
|
|
81
|
+
if (pointers) return pointers;
|
|
82
|
+
pointers = new Set();
|
|
83
|
+
for (const m of text.matchAll(/\b(?:\d\d\s+)?([A-Z0-9-]+)\b[^.\n]{0,80}\bUSAGE\s+(?:IS\s+)?(?:PROCEDURE-POINTER|POINTER)\b/gi)) {
|
|
84
|
+
pointers.add(m[1].toUpperCase());
|
|
85
|
+
}
|
|
86
|
+
return pointers;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// How many newlines of the joined text come before an offset, which is the normalised entry
|
|
90
|
+
// holding it.
|
|
91
|
+
const starts = [0];
|
|
92
|
+
for (let i = text.indexOf('\n'); i >= 0; i = text.indexOf('\n', i + 1)) starts.push(i + 1);
|
|
93
|
+
const entryAt = (offset) => {
|
|
94
|
+
let lo = 0;
|
|
95
|
+
let hi = starts.length - 1;
|
|
96
|
+
while (lo < hi) {
|
|
97
|
+
const mid = (lo + hi + 1) >> 1;
|
|
98
|
+
if (starts[mid] <= offset) lo = mid; else hi = mid - 1;
|
|
99
|
+
}
|
|
100
|
+
return lo;
|
|
101
|
+
};
|
|
82
102
|
|
|
83
103
|
let hit = false;
|
|
84
104
|
for (const p of PATTERNS) {
|
|
85
105
|
p.re.lastIndex = 0;
|
|
86
106
|
let m;
|
|
87
107
|
while ((m = p.re.exec(text)) !== null) {
|
|
88
|
-
if (p.needsPointerOperand && !
|
|
108
|
+
if (p.needsPointerOperand && !pointerNames().has(String(m[1]).toUpperCase())) continue;
|
|
89
109
|
// The line number is the normalised entry's own, so a construct that came from a copybook
|
|
90
110
|
// is reported where the compiler would see it.
|
|
91
|
-
const before =
|
|
111
|
+
const before = entryAt(m.index);
|
|
92
112
|
const line = norm.entries[before] ? norm.entries[before].line : 1;
|
|
93
113
|
findings.push({ rule: p.rule, path, line, detail: p.detail(m) });
|
|
94
114
|
hit = true;
|
package/lib/sets/pli.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// PL/I programs read statement by statement (lib/pli): where they address storage through a pointer,
|
|
3
|
+
// call through an entry variable or use the preprocessor, which a static reading cannot follow, and
|
|
4
|
+
// ON units that swallow their condition. Data flow through PL/I is the flow set's; this set reports
|
|
5
|
+
// what the program text itself shows, and counts every statement the reader could not parse.
|
|
6
|
+
import { inScope, isPli, relPath } from '../sources.mjs';
|
|
7
|
+
import { report } from '../kernel/ruleset.mjs';
|
|
8
|
+
import { treeFor, noteUnread } from '../kernel/source-tree.mjs';
|
|
9
|
+
import { eachWithinMemory } from '../kernel/memory.mjs';
|
|
10
|
+
import { readPli } from '../pli/lex.mjs';
|
|
11
|
+
import { parseStatement } from '../pli/statements.mjs';
|
|
12
|
+
import { PLI_RULES, checkProgram } from '../pli/rules/index.mjs';
|
|
13
|
+
|
|
14
|
+
export { PLI_RULES };
|
|
15
|
+
|
|
16
|
+
export function scanPli(root, opts = {}) {
|
|
17
|
+
const tree = treeFor(root, opts);
|
|
18
|
+
const files = tree.list().filter(inScope(opts)).filter(isPli);
|
|
19
|
+
const stats = { filesScanned: 0, filesUnreadable: 0, statements: 0, statementsParsed: 0 };
|
|
20
|
+
const notParsed = {};
|
|
21
|
+
const findings = [];
|
|
22
|
+
const run = eachWithinMemory(files, (f) => {
|
|
23
|
+
let src;
|
|
24
|
+
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
25
|
+
const path = relPath(root, f);
|
|
26
|
+
const statements = readPli(src, { file: path }).statements.map((st) => ({ ...st, parsed: parseStatement(st) }));
|
|
27
|
+
stats.filesScanned++;
|
|
28
|
+
for (const st of statements) {
|
|
29
|
+
stats.statements++;
|
|
30
|
+
if (st.parsed.status === 'parsed') { stats.statementsParsed++; continue; }
|
|
31
|
+
(notParsed[st.parsed.kind || 'UNKNOWN'] ||= { count: 0, first: `${path}:${st.line}` }).count++;
|
|
32
|
+
}
|
|
33
|
+
findings.push(...checkProgram({ path, statements }));
|
|
34
|
+
return src.length;
|
|
35
|
+
}, { label: 'pli', maxBytes: opts.maxSourceBytes ?? Infinity });
|
|
36
|
+
|
|
37
|
+
// A statement the reader could not parse may hold a pointer, an entry call or an ON unit.
|
|
38
|
+
if (Object.keys(notParsed).length) { stats.statementsNotParsed = notParsed; stats.coverageIncomplete = true; }
|
|
39
|
+
return report('pli', { rules: PLI_RULES, findings, stats, run });
|
|
40
|
+
}
|
package/lib/sets/priv.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { inScope, isProgram, relPath } from '../sources.mjs';
|
|
20
20
|
import { report } from '../kernel/ruleset.mjs';
|
|
21
21
|
import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
|
|
22
|
-
import {
|
|
22
|
+
import { drive, loopOver } from '../kernel/shared-pass.mjs';
|
|
23
23
|
import { parseCsd } from '../csd.mjs';
|
|
24
24
|
import { parseJcl } from '../jcl.mjs';
|
|
25
25
|
import { loadSite } from '../site.mjs';
|
|
@@ -101,7 +101,7 @@ function under(dsn, prefixes) {
|
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export function
|
|
104
|
+
export function* scanPrivSteps(root, opts = {}) {
|
|
105
105
|
const site = loadSite(root, opts.site || null, opts.tree);
|
|
106
106
|
const tree = treeFor(root, opts);
|
|
107
107
|
const files = tree.list().filter(inScope(opts));
|
|
@@ -140,7 +140,7 @@ export function scanPriv(root, opts = {}) {
|
|
|
140
140
|
const diagnostics = [];
|
|
141
141
|
const uss = [];
|
|
142
142
|
|
|
143
|
-
const run =
|
|
143
|
+
const run = yield loopOver(files, (f) => {
|
|
144
144
|
let src;
|
|
145
145
|
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
146
146
|
const path = relPath(root, f);
|
|
@@ -320,3 +320,5 @@ export function scanPriv(root, opts = {}) {
|
|
|
320
320
|
|
|
321
321
|
return report('priv', { rules: PRIV_RULES, findings, stats, run });
|
|
322
322
|
}
|
|
323
|
+
|
|
324
|
+
export const scanPriv = (root, opts = {}) => drive(scanPrivSteps(root, opts));
|
package/lib/sets/recon.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { inScope, isJcl, isProgram, isCopybook, relPath } from '../sources.mjs';
|
|
15
15
|
import { report } from '../kernel/ruleset.mjs';
|
|
16
16
|
import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
|
|
17
|
-
import {
|
|
17
|
+
import { drive, loopOver } from '../kernel/shared-pass.mjs';
|
|
18
18
|
import { loadSite, classifyPath, productionQualifierOf, SITE_FILE } from '../site.mjs';
|
|
19
19
|
import { parseJcl, dispositionOf } from '../jcl.mjs';
|
|
20
20
|
import { statementCard } from '../cards.mjs';
|
|
@@ -161,7 +161,7 @@ function productionTouches(job, qualifiers) {
|
|
|
161
161
|
return { found, claimed };
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
export function
|
|
164
|
+
export function* scanReconSteps(root, opts = {}) {
|
|
165
165
|
const site = loadSite(root, opts.site || null, opts.tree);
|
|
166
166
|
const tree = treeFor(root, opts);
|
|
167
167
|
const files = tree.list().filter(inScope(opts))
|
|
@@ -192,7 +192,7 @@ export function scanRecon(root, opts = {}) {
|
|
|
192
192
|
|
|
193
193
|
// This set reads every program, copybook and job in the tree, which is the widest reading any
|
|
194
194
|
// set does, so it is walked inside the guard like the rest.
|
|
195
|
-
const run =
|
|
195
|
+
const run = yield loopOver(files, (f) => {
|
|
196
196
|
let src;
|
|
197
197
|
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
198
198
|
stats.filesScanned++;
|
|
@@ -267,3 +267,5 @@ export function scanRecon(root, opts = {}) {
|
|
|
267
267
|
// production means, as against files going unread. Both travel, and they are not merged.
|
|
268
268
|
return report('recon', { rules: RECON_RULES, findings, stats, run });
|
|
269
269
|
}
|
|
270
|
+
|
|
271
|
+
export const scanRecon = (root, opts = {}) => drive(scanReconSteps(root, opts));
|