@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/inventory.mjs
CHANGED
|
@@ -3,12 +3,13 @@ import { detectFormat } from './parser.mjs';
|
|
|
3
3
|
import { inScope, isCopybook, isJcl, isProgram, relPath } from './sources.mjs';
|
|
4
4
|
import { SCHEMA_VERSION, TOOL_VERSION } from './version.mjs';
|
|
5
5
|
import { treeFor } from './kernel/source-tree.mjs';
|
|
6
|
+
import { drive, loopOver } from './kernel/shared-pass.mjs';
|
|
6
7
|
|
|
7
8
|
const SYSTEM_COPY = /^(?:(?:DFH|DSN|CEE|IGZ|EZA|BPX|CSQ|CMQ|DLI)[A-Z0-9$#@]{0,5}|SQLCA|SQLDA|ATTRIB)$/i;
|
|
8
9
|
|
|
9
10
|
// What the repository contains and, as importantly, what could not be read: a scan over a tree
|
|
10
11
|
// whose copybooks are missing has not examined what it appears to have examined.
|
|
11
|
-
export function
|
|
12
|
+
export function* inventorySteps(root, opts = {}) {
|
|
12
13
|
const tree = treeFor(root, opts);
|
|
13
14
|
const idx = tree.index;
|
|
14
15
|
const files = tree.list().filter(inScope(opts));
|
|
@@ -28,27 +29,27 @@ export function inventory(root, opts = {}) {
|
|
|
28
29
|
ebcdic: [],
|
|
29
30
|
recursiveCopybooks: {},
|
|
30
31
|
};
|
|
31
|
-
|
|
32
|
+
yield loopOver(files, (f) => {
|
|
32
33
|
if (isCopybook(f)) out.summary.copybookFiles++;
|
|
33
34
|
if (isJcl(f)) out.summary.jclFiles++;
|
|
34
|
-
if (!isProgram(f))
|
|
35
|
+
if (!isProgram(f)) return;
|
|
35
36
|
out.summary.programFiles++;
|
|
36
37
|
let src;
|
|
37
38
|
try {
|
|
38
39
|
const s = tree.text(f);
|
|
39
40
|
src = s.text;
|
|
40
41
|
if (s.encoding === 'ebcdic') { out.summary.filesEbcdic++; out.ebcdic.push(tree.rel(f)); }
|
|
41
|
-
} catch (e) { out.summary.filesUnreadable++; out.unreadable.push(`${tree.rel(f)}: ${e.code || e.name}`);
|
|
42
|
+
} catch (e) { out.summary.filesUnreadable++; out.unreadable.push(`${tree.rel(f)}: ${e.code || e.name}`); return; }
|
|
42
43
|
// A file with a program extension and no program in it is usually a copybook named .cbl; it is
|
|
43
44
|
// counted so the difference between program files and programs read is accounted for.
|
|
44
|
-
if (!/PROCEDURE\s+DIVISION|PROGRAM-ID/i.test(src)) { out.summary.notPrograms++;
|
|
45
|
+
if (!/PROCEDURE\s+DIVISION|PROGRAM-ID/i.test(src)) { out.summary.notPrograms++; return; }
|
|
45
46
|
let res;
|
|
46
47
|
try {
|
|
47
48
|
res = tree.parse(f, src);
|
|
48
49
|
} catch (e) {
|
|
49
50
|
out.summary.filesUnreadable++;
|
|
50
51
|
out.unreadable.push(`${tree.rel(f)}: ${e.code || e.name}`);
|
|
51
|
-
|
|
52
|
+
return;
|
|
52
53
|
}
|
|
53
54
|
out.summary.filesScanned++;
|
|
54
55
|
out.summary.programs += res.programs.length;
|
|
@@ -71,8 +72,10 @@ export function inventory(root, opts = {}) {
|
|
|
71
72
|
out.missingCopybooks[k] = (out.missingCopybooks[k] || 0) + 1;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
|
-
}
|
|
75
|
+
}, { guarded: false, label: 'inventory' });
|
|
75
76
|
out.summary.nosrc = out.summary.filesScanned === 0;
|
|
76
77
|
out.summary.coverageIncomplete = out.summary.copiesMissing > 0 || out.summary.copiesRecursive > 0 || (out.summary.copiesOverLimit || 0) > 0 || out.summary.filesUnreadable > 0 || out.summary.copiesRefused > 0 || out.summary.dirsUnreadable > 0 || out.summary.symlinks.outside > 0;
|
|
77
78
|
return out;
|
|
78
79
|
}
|
|
80
|
+
|
|
81
|
+
export const inventory = (root, opts = {}) => drive(inventorySteps(root, opts));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// What cobolwork reads from ironwork, and from which ironwork release: the formats it keys on, and
|
|
3
|
+
// how ironwork says it ended a run. The run endings are ironwork's docs/run-endings.tsv; a test holds
|
|
4
|
+
// them to the copy in test/fixtures/ironwork, and CI holds that copy to ironwork's.
|
|
5
|
+
|
|
6
|
+
// Each format by the id its reader keys on, with the first ironwork release that writes all of what
|
|
7
|
+
// cobolwork reads from it. A reader refuses an id it does not know, so a later format is not misread.
|
|
8
|
+
export const IRONWORK_FORMATS = {
|
|
9
|
+
'ironwork-fuzz/v1': '0.3.0',
|
|
10
|
+
'ironwork-fuzz-interface/v1': '0.5.0',
|
|
11
|
+
'cobolwork-evidence/v1': '0.4.0',
|
|
12
|
+
'https://github.com/Portll/ironwork/blob/main/docs/evidence.md#equivalence-v1': '0.1.2',
|
|
13
|
+
'run-endings': '0.4.1',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// The first ironwork release whose output cobolwork reads throughout; a format listed with a later
|
|
17
|
+
// release is read from that release on.
|
|
18
|
+
export const IRONWORK_MINIMUM = '0.4.1';
|
|
19
|
+
|
|
20
|
+
// How ironwork ends a run other than with its RETURN-CODE, by exit status.
|
|
21
|
+
export const RUN_ENDINGS = { 240: 'abend', 241: 'refused', 242: 'not-generated', 243: 'stopped', 244: 'not-run', 245: 'unreadable', 246: 'usage', 255: 'internal' };
|
|
22
|
+
|
|
23
|
+
// The abend codes that say a run reached what ironwork does not run.
|
|
24
|
+
export const NOT_RUN_ABENDS = ['IRONWORK', 'EXEC', 'JAVA'];
|
package/lib/ironwork.mjs
CHANGED
|
@@ -15,18 +15,24 @@ import { EIB_FIELDS, DIB_FIELDS, SQLCA_FIELDS } from './words.mjs';
|
|
|
15
15
|
// 12 S, 16 U. Only 0 and 4 produce a program.
|
|
16
16
|
const COMPILED = new Set([0, 4]);
|
|
17
17
|
const REJECTED = new Set([8, 12, 16]);
|
|
18
|
+
// ironwork grades a refusal by name as severe (12), so a run that ends at 8 holds only IBM-graded
|
|
19
|
+
// errors in the program, whatever their wording says; the wording decides only at 12 and 16.
|
|
20
|
+
const GRADED_ERROR = 8;
|
|
18
21
|
// Warnings and informational lines follow the errors, and say nothing about why a program failed.
|
|
19
|
-
|
|
22
|
+
// The label follows the position, or opens a line that has none.
|
|
23
|
+
const NOT_AN_ERROR = /(?:^|: )(?:warning|informational): /;
|
|
24
|
+
// ironwork opens a message with its id and severity, `IWC0101-S`, as IBM opens its own.
|
|
25
|
+
const MESSAGE_ID = /^((?:IW[A-Z]|IGY[A-Z]{2})\d{4})-[IWESU] (.*)$/;
|
|
20
26
|
const PER_PROGRAM_MS = 60000;
|
|
21
27
|
const MAX_LISTED = 10;
|
|
22
28
|
|
|
23
|
-
// ironwork refuses by name what it does not model yet; that says nothing about the
|
|
24
|
-
// the CICS, DL/I or SQL translator declares it reports as undefined, without saying
|
|
25
|
-
// read as its gap, not the program's.
|
|
29
|
+
// ironwork refuses by name what it does not model yet, with an IWR id; that says nothing about the
|
|
30
|
+
// program. A name the CICS, DL/I or SQL translator declares it reports as undefined, without saying
|
|
31
|
+
// so, and that is read as its gap, not the program's.
|
|
26
32
|
const NOT_SUPPORTED = /\bnot supported\b/i;
|
|
27
33
|
const TRANSLATOR_NAMES = [EIB_FIELDS, DIB_FIELDS, SQLCA_FIELDS];
|
|
28
|
-
function notModelled(message) {
|
|
29
|
-
if (NOT_SUPPORTED.test(message)) return true;
|
|
34
|
+
function notModelled({ id, message }) {
|
|
35
|
+
if (id?.startsWith('IWR') || NOT_SUPPORTED.test(message)) return true;
|
|
30
36
|
const name = /^([A-Z][A-Z0-9-]*) is not defined$/.exec(message);
|
|
31
37
|
return !!name && TRANSLATOR_NAMES.some((s) => s.has(name[1]));
|
|
32
38
|
}
|
|
@@ -42,9 +48,10 @@ function diagnostic(line, programPath, rel) {
|
|
|
42
48
|
if (!m) return { path: rel, message: printable(redact(line), 200) };
|
|
43
49
|
const [file, ln, col, text] = m.length === 5 ? [m[1], Number(m[2]), Number(m[3]), m[4]] : [m[1], null, null, m[2]];
|
|
44
50
|
const inProgram = resolve(file) === programPath;
|
|
51
|
+
const [, id, said] = MESSAGE_ID.exec(text) || [null, null, text];
|
|
45
52
|
return {
|
|
46
|
-
path: rel, ...(ln ? { line: ln, col } : {}), ...(inProgram ? {} : { member: printable(file, 80) }),
|
|
47
|
-
message: printable(redact(
|
|
53
|
+
path: rel, ...(ln ? { line: ln, col } : {}), ...(inProgram ? {} : { member: printable(file, 80) }), ...(id ? { id } : {}),
|
|
54
|
+
message: printable(redact(said), 200),
|
|
48
55
|
};
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -57,9 +64,9 @@ export function ironworkVersion(path, { env = process.env } = {}) {
|
|
|
57
64
|
// estate's copy libraries as -I, and sorts the programs into those it accepts, those it rejects,
|
|
58
65
|
// and those it could not decide: a construct it does not model, a copybook the tree lacks, or a
|
|
59
66
|
// run that ended some other way.
|
|
60
|
-
export function checkWithIronwork(path, root, { allow = null, copylibs = [], env = process.env } = {}) {
|
|
61
|
-
const programs = [...new Set(directoryTree(root).list())].filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
|
|
62
|
-
const libraries = [...buildFileIndex(root).copyDirs, ...copylibs].flatMap((d) => ['-I', d]);
|
|
67
|
+
export function checkWithIronwork(path, root, { allow = null, programs: given = null, copyDirs = null, copylibs = [], env = process.env } = {}) {
|
|
68
|
+
const programs = given || [...new Set(directoryTree(root).list())].filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
|
|
69
|
+
const libraries = [...(copyDirs || buildFileIndex(root).copyDirs), ...copylibs].flatMap((d) => ['-I', d]);
|
|
63
70
|
const out = { programs: programs.length, accepted: 0, warned: 0, failed: [], notModelled: [], unresolved: [], unrun: [] };
|
|
64
71
|
for (const file of programs) {
|
|
65
72
|
const rel = relPath(root, file);
|
|
@@ -73,7 +80,7 @@ export function checkWithIronwork(path, root, { allow = null, copylibs = [], env
|
|
|
73
80
|
continue;
|
|
74
81
|
}
|
|
75
82
|
const found = String(r.stderr || '').split(/\r?\n/).filter((l) => l && !NOT_AN_ERROR.test(l)).map((l) => diagnostic(l, resolve(file), rel));
|
|
76
|
-
const errors = found.filter((d) => !notModelled(d
|
|
83
|
+
const errors = found.filter((d) => !(r.status !== GRADED_ERROR && notModelled(d)) && !UNRESOLVED.test(d.message));
|
|
77
84
|
if (errors.length) out.failed.push({ ...errors[0], errors: errors.length });
|
|
78
85
|
else if (found.some((d) => UNRESOLVED.test(d.message))) out.unresolved.push(found.find((d) => UNRESOLVED.test(d.message)));
|
|
79
86
|
else out.notModelled.push(found[0] || { path: rel, message: `ironwork exited ${r.status} and gave no error` });
|
|
@@ -90,7 +97,7 @@ export function ironworkVerdict(result) {
|
|
|
90
97
|
|
|
91
98
|
export function ironworkReasons(result) {
|
|
92
99
|
const reasons = [];
|
|
93
|
-
const at = (d) => `${d.path}${d.line ? `:${d.line}:${d.col}` : ''}${d.member ? ` (in ${d.member})` : ''}`;
|
|
100
|
+
const at = (d) => `${d.path}${d.line ? `:${d.line}:${d.col}` : ''}${d.member ? ` (in ${d.member})` : ''}${d.id ? ` ${d.id}` : ''}`;
|
|
94
101
|
if (result.failed.length) {
|
|
95
102
|
reasons.push(`ironwork check: ${result.failed.length} of ${result.programs} program(s) do not compile as Enterprise COBOL`);
|
|
96
103
|
for (const d of result.failed.slice(0, MAX_LISTED)) reasons.push(`${at(d)}: ${d.message}`);
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Data sets brought off z/OS whole: a TSO TRANSMIT (XMIT) file, and an IEBCOPY unload data set, alone
|
|
3
|
+
// or inside one. Each partitioned data set's members come back as their records, in EBCDIC, ended by
|
|
4
|
+
// NL (X'15'), as a z/OSMF record download gives them; a sequential data set comes back as one file.
|
|
5
|
+
//
|
|
6
|
+
// The layouts are IBM's: z/OS TSO/E Customization, "Format of transmitted data", "Control record
|
|
7
|
+
// formats" and "Numeric values" (the text units); z/OS DFSMSdfp Utilities, "Unload partitioned data
|
|
8
|
+
// set format" (COPYR1, COPYR2, directory and member data records); z/OS DFSMSdfp Advanced Services,
|
|
9
|
+
// "Data Extent Block (DEB) Fields" and "Device Characteristics Information" (DEVTYPE), which turn a
|
|
10
|
+
// member block's MBBCCHHR into the TTR its directory entry holds.
|
|
11
|
+
import { ebcdicByte } from '../sources.mjs';
|
|
12
|
+
|
|
13
|
+
const EOR = 0x15;
|
|
14
|
+
const UNLOAD_ID = [0xca, 0x6d, 0x0f];
|
|
15
|
+
const ebcdic = (s) => Buffer.from([...s].map((c) => ebcdicByte(c)));
|
|
16
|
+
const INMR = (n) => ebcdic(`INMR0${n}`);
|
|
17
|
+
|
|
18
|
+
const KEY = { INMDSNAM: 0x0002, INMDSORG: 0x003c, INMLRECL: 0x0042, INMRECFM: 0x0049, INMUTILN: 0x1028, INMNUMF: 0x102f };
|
|
19
|
+
const DSORG_PO = 0x0200;
|
|
20
|
+
|
|
21
|
+
const textOf = (buf) => [...buf].map((b) => EBCDIC_TEXT[b]).join('');
|
|
22
|
+
const EBCDIC_TEXT = (() => {
|
|
23
|
+
const t = new Array(256).fill('?');
|
|
24
|
+
for (let c = 0x20; c < 0x7f; c++) {
|
|
25
|
+
const b = ebcdicByte(String.fromCharCode(c));
|
|
26
|
+
if (b !== null) t[b] = String.fromCharCode(c);
|
|
27
|
+
}
|
|
28
|
+
return t;
|
|
29
|
+
})();
|
|
30
|
+
const uint = (buf) => buf.reduce((n, b) => n * 256 + b, 0);
|
|
31
|
+
const isUnloadId = (buf, at) => buf.length >= at + 3 && UNLOAD_ID.every((b, i) => buf[at + i] === b);
|
|
32
|
+
|
|
33
|
+
// What a file holds, from its first bytes: an XMIT file's first segment is its INMR01 header record;
|
|
34
|
+
// an unload's COPYR1 carries X'CA6D0F' after its flag byte, with a BDW and SDW, an RDW, or nothing
|
|
35
|
+
// before it.
|
|
36
|
+
export function archiveKind(head) {
|
|
37
|
+
if (head.length >= 8 && (head[1] & 0xa0) === 0xa0 && head.subarray(2, 8).equals(INMR(1))) return 'xmit';
|
|
38
|
+
for (const at of [1, 5, 9]) if (isUnloadId(head, at)) return 'unload';
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The transmission's logical records, each from its segments: a length byte counting itself and the
|
|
43
|
+
// flag byte, the flags (X'80' first, X'40' last, X'20' control record) and up to 253 bytes.
|
|
44
|
+
function xmitRecords(buf) {
|
|
45
|
+
const records = [];
|
|
46
|
+
let parts = null;
|
|
47
|
+
let control = false;
|
|
48
|
+
for (let at = 0; at < buf.length;) {
|
|
49
|
+
const n = buf[at];
|
|
50
|
+
if (n < 2 || at + n > buf.length) break;
|
|
51
|
+
const flags = buf[at + 1];
|
|
52
|
+
if (flags & 0x80) { parts = []; control = (flags & 0x20) !== 0; }
|
|
53
|
+
if (parts) parts.push(buf.subarray(at + 2, at + n));
|
|
54
|
+
if (flags & 0x40 && parts) {
|
|
55
|
+
const bytes = Buffer.concat(parts);
|
|
56
|
+
records.push({ control, bytes });
|
|
57
|
+
parts = null;
|
|
58
|
+
if (control && bytes.subarray(0, 6).equals(INMR(6))) break;
|
|
59
|
+
}
|
|
60
|
+
at += n;
|
|
61
|
+
}
|
|
62
|
+
return records;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Text units from `at`: a key, a count, and that many length and data pairs.
|
|
66
|
+
function textUnits(bytes, at) {
|
|
67
|
+
const units = new Map();
|
|
68
|
+
while (at + 4 <= bytes.length) {
|
|
69
|
+
const key = bytes.readUInt16BE(at);
|
|
70
|
+
const count = bytes.readUInt16BE(at + 2);
|
|
71
|
+
at += 4;
|
|
72
|
+
const fields = [];
|
|
73
|
+
for (let i = 0; i < count && at + 2 <= bytes.length; i++) {
|
|
74
|
+
const len = bytes.readUInt16BE(at);
|
|
75
|
+
fields.push(bytes.subarray(at + 2, at + 2 + len));
|
|
76
|
+
at += 2 + len;
|
|
77
|
+
}
|
|
78
|
+
units.set(key, fields);
|
|
79
|
+
}
|
|
80
|
+
return units;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readXmit(buf) {
|
|
84
|
+
const records = xmitRecords(buf);
|
|
85
|
+
const last = records[records.length - 1];
|
|
86
|
+
if (!last?.control || !last.bytes.subarray(0, 6).equals(INMR(6))) return { why: 'the transmission ends before its INMR06 trailer' };
|
|
87
|
+
const header = textUnits(records[0].bytes, 6);
|
|
88
|
+
const files = header.get(KEY.INMNUMF)?.[0];
|
|
89
|
+
if (files && uint(files) > 1) return { why: `the transmission holds ${uint(files)} files, and cobolwork reads one` };
|
|
90
|
+
const utilities = records.filter((r) => r.control && r.bytes.subarray(0, 6).equals(INMR(2))).map((r) => textUnits(r.bytes, 10));
|
|
91
|
+
const named = utilities.find((u) => u.has(KEY.INMDSNAM));
|
|
92
|
+
const dataSet = named ? named.get(KEY.INMDSNAM).map(textOf).join('.') : null;
|
|
93
|
+
const start = records.findIndex((r) => r.control && r.bytes.subarray(0, 6).equals(INMR(3)));
|
|
94
|
+
if (start < 0) return { why: 'the transmission has no INMR03 data control record' };
|
|
95
|
+
const data = records.slice(start + 1, -1).filter((r) => !r.control).map((r) => r.bytes);
|
|
96
|
+
const unloaded = utilities.some((u) => textOf(u.get(KEY.INMUTILN)?.[0] || Buffer.alloc(0)) === 'IEBCOPY');
|
|
97
|
+
if (unloaded) return { ...readUnloadRecords(data), kind: 'xmit', dataSet };
|
|
98
|
+
const dsorg = utilities.map((u) => u.get(KEY.INMDSORG)?.[0]).find(Boolean);
|
|
99
|
+
if (dsorg && uint(dsorg) === DSORG_PO) return { why: 'a partitioned data set transmitted without IEBCOPY' };
|
|
100
|
+
return { kind: 'xmit', dataSet, sequential: true, members: [{ name: null, bytes: joined(data) }], aliases: [] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// An unload stored with its descriptors: each block a BDW, then segments, each an SDW whose third
|
|
104
|
+
// byte's low two bits say whether it is a whole record (0), its first (1), last (2) or middle (3)
|
|
105
|
+
// part; or each record after an RDW; or the records with nothing between them, COPYR1 and COPYR2 at
|
|
106
|
+
// their fixed lengths and everything after them one stream.
|
|
107
|
+
function unloadRecords(buf) {
|
|
108
|
+
if (isUnloadId(buf, 9)) {
|
|
109
|
+
const records = [];
|
|
110
|
+
let parts = [];
|
|
111
|
+
for (let at = 0; at + 4 <= buf.length;) {
|
|
112
|
+
const end = at + buf.readUInt16BE(at);
|
|
113
|
+
if (end <= at || end > buf.length) break;
|
|
114
|
+
for (let s = at + 4; s + 4 <= end;) {
|
|
115
|
+
const len = buf.readUInt16BE(s);
|
|
116
|
+
if (len < 4) break;
|
|
117
|
+
const code = buf[s + 2] & 0x03;
|
|
118
|
+
parts.push(buf.subarray(s + 4, s + len));
|
|
119
|
+
if (code === 0 || code === 2) { records.push(Buffer.concat(parts)); parts = []; }
|
|
120
|
+
s += len;
|
|
121
|
+
}
|
|
122
|
+
at = end;
|
|
123
|
+
}
|
|
124
|
+
return records;
|
|
125
|
+
}
|
|
126
|
+
if (isUnloadId(buf, 5)) {
|
|
127
|
+
const records = [];
|
|
128
|
+
for (let at = 0; at + 4 <= buf.length;) {
|
|
129
|
+
const len = buf.readUInt16BE(at);
|
|
130
|
+
if (len < 4 || at + len > buf.length) break;
|
|
131
|
+
records.push(buf.subarray(at + 4, at + len));
|
|
132
|
+
at += len;
|
|
133
|
+
}
|
|
134
|
+
return records;
|
|
135
|
+
}
|
|
136
|
+
return [buf.subarray(0, COPYR1_LENGTH), buf.subarray(COPYR1_LENGTH, COPYR1_LENGTH + COPYR2_LENGTH), buf.subarray(COPYR1_LENGTH + COPYR2_LENGTH)];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const COPYR1_LENGTH = 56;
|
|
140
|
+
const COPYR2_LENGTH = 276;
|
|
141
|
+
const DIRECTORY_BLOCK = 276;
|
|
142
|
+
const BLOCK_HEADER = 12;
|
|
143
|
+
|
|
144
|
+
export function readArchive(buf) {
|
|
145
|
+
const kind = archiveKind(buf.subarray(0, 16));
|
|
146
|
+
if (kind === 'xmit') return readXmit(buf);
|
|
147
|
+
if (kind === 'unload') return { ...readUnloadRecords(unloadRecords(buf)), kind: 'unload', dataSet: null };
|
|
148
|
+
return { why: 'neither an XMIT file nor an IEBCOPY unload' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// COPYR1 and COPYR2, then the directory blocks and every member's blocks, which run on across
|
|
152
|
+
// records: a directory block is a 12-byte count, the 8-byte key and 256 bytes of entries, and the
|
|
153
|
+
// directory ends with 12 bytes of zeros; a member's blocks are each a flag byte, MBB, CCHHR, key
|
|
154
|
+
// length, data length, key and data, and its last is one with no key and no data.
|
|
155
|
+
function readUnloadRecords(records) {
|
|
156
|
+
const [r1, r2] = records;
|
|
157
|
+
if (!r1 || !isUnloadId(r1, 1)) return { why: 'the unload has no COPYR1 record' };
|
|
158
|
+
const format = r1[0] >> 6;
|
|
159
|
+
if (format === 2) return { why: 'IEBCOPY marked the unload incomplete or in error' };
|
|
160
|
+
if (format !== 0 || r1[0] & 0x01) return { why: 'a PDSE unload, whose attribute records cobolwork does not read yet' };
|
|
161
|
+
const headers = r1.length >= 38 ? r1.readUInt16BE(36) || 2 : 2;
|
|
162
|
+
if (headers !== 2) return { why: `the unload has ${headers} header records, and cobolwork reads two` };
|
|
163
|
+
if (!r2 || r2.length < 16 + 16) return { why: 'the unload has no COPYR2 record' };
|
|
164
|
+
const lrecl = r1.readUInt16BE(8);
|
|
165
|
+
const recfm = r1[10] >> 6;
|
|
166
|
+
const tracksPerCylinder = r1.readUInt16BE(26);
|
|
167
|
+
const extents = [];
|
|
168
|
+
for (let at = 16; at + 16 <= r2.length && extents.length < 16; at += 16) {
|
|
169
|
+
extents.push({ cyl: cylinder(r2.readUInt16BE(at + 6), r2.readUInt16BE(at + 8)), trk: r2.readUInt16BE(at + 8) & 0x0f, tracks: r2[at + 5] * 0x10000 + r2.readUInt16BE(at + 14) });
|
|
170
|
+
}
|
|
171
|
+
const stream = Buffer.concat(records.slice(2));
|
|
172
|
+
let at = 0;
|
|
173
|
+
const entries = [];
|
|
174
|
+
for (;;) {
|
|
175
|
+
if (at + BLOCK_HEADER > stream.length) return { why: 'the directory runs past the end of the unload' };
|
|
176
|
+
if (stream.subarray(at, at + BLOCK_HEADER).every((b) => b === 0)) { at += BLOCK_HEADER; break; }
|
|
177
|
+
const block = stream.subarray(at + BLOCK_HEADER + 8, at + DIRECTORY_BLOCK);
|
|
178
|
+
const used = Math.min(block.readUInt16BE(0), block.length);
|
|
179
|
+
for (let e = 2; e + 12 <= used;) {
|
|
180
|
+
const name = block.subarray(e, e + 8);
|
|
181
|
+
if (name.every((b) => b === 0xff)) break;
|
|
182
|
+
const c = block[e + 11];
|
|
183
|
+
entries.push({ name: textOf(name).trimEnd(), ttr: uint(block.subarray(e + 8, e + 11)), alias: (c & 0x80) !== 0 });
|
|
184
|
+
e += 12 + (c & 0x1f) * 2;
|
|
185
|
+
}
|
|
186
|
+
at += DIRECTORY_BLOCK;
|
|
187
|
+
}
|
|
188
|
+
const byTtr = new Map(entries.filter((e) => !e.alias).map((e) => [e.ttr, e.name]));
|
|
189
|
+
const members = [];
|
|
190
|
+
let unmatched = 0;
|
|
191
|
+
while (at + BLOCK_HEADER <= stream.length) {
|
|
192
|
+
let ttr = null;
|
|
193
|
+
const blocks = [];
|
|
194
|
+
for (;;) {
|
|
195
|
+
if (at + BLOCK_HEADER > stream.length) return { why: 'a member runs past the end of the unload' };
|
|
196
|
+
const h = stream.subarray(at, at + BLOCK_HEADER);
|
|
197
|
+
const keyLength = h[9];
|
|
198
|
+
const dataLength = h.readUInt16BE(10);
|
|
199
|
+
at += BLOCK_HEADER;
|
|
200
|
+
if (keyLength === 0 && dataLength === 0) break;
|
|
201
|
+
if (ttr === null) ttr = relativeTrack(extents, tracksPerCylinder, h) * 256 + h[8];
|
|
202
|
+
blocks.push(stream.subarray(at + keyLength, at + keyLength + dataLength));
|
|
203
|
+
at += keyLength + dataLength;
|
|
204
|
+
}
|
|
205
|
+
if (!blocks.length) continue;
|
|
206
|
+
const name = byTtr.get(ttr);
|
|
207
|
+
if (name === undefined) { unmatched++; continue; }
|
|
208
|
+
members.push({ name, bytes: joined(blocks.flatMap((b) => recordsOf(b, recfm, lrecl))) });
|
|
209
|
+
}
|
|
210
|
+
const held = new Set(members.map((m) => m.name));
|
|
211
|
+
return {
|
|
212
|
+
members,
|
|
213
|
+
aliases: entries.filter((e) => e.alias).map((e) => e.name),
|
|
214
|
+
...(unmatched ? { unmatched } : {}),
|
|
215
|
+
...(entries.some((e) => !e.alias && !held.has(e.name)) ? { missing: entries.filter((e) => !e.alias && !held.has(e.name)).map((e) => e.name) } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A cylinder number from CC and HH, whose high 12 bits hold the cylinder's high bits on an extended
|
|
220
|
+
// address volume (DEBSTRHH).
|
|
221
|
+
const cylinder = (cc, hh) => (hh >> 4) * 0x10000 + cc;
|
|
222
|
+
|
|
223
|
+
// The track's number from the start of the data set: the tracks of the extents before extent M, then
|
|
224
|
+
// the cylinders and tracks into M.
|
|
225
|
+
function relativeTrack(extents, tracksPerCylinder, h) {
|
|
226
|
+
const m = h[1];
|
|
227
|
+
const e = extents[m];
|
|
228
|
+
if (!e) return -1;
|
|
229
|
+
const before = extents.slice(0, m).reduce((n, x) => n + x.tracks, 0);
|
|
230
|
+
const hh = h.readUInt16BE(6);
|
|
231
|
+
return before + (cylinder(h.readUInt16BE(4), hh) - e.cyl) * tracksPerCylinder + ((hh & 0x0f) - e.trk);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// A physical block's records: fixed-length records LRECL at a time, variable ones after the BDW each
|
|
235
|
+
// by its RDW, an undefined block as one record. RECFM's first two bits: 10 fixed, 01 variable, 11
|
|
236
|
+
// undefined.
|
|
237
|
+
function recordsOf(block, recfm, lrecl) {
|
|
238
|
+
if (recfm === 2 && lrecl > 0) {
|
|
239
|
+
const out = [];
|
|
240
|
+
for (let i = 0; i + lrecl <= block.length; i += lrecl) out.push(block.subarray(i, i + lrecl));
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
if (recfm === 1) {
|
|
244
|
+
const out = [];
|
|
245
|
+
for (let i = 4; i + 4 <= block.length;) {
|
|
246
|
+
const len = block.readUInt16BE(i);
|
|
247
|
+
if (len < 4) break;
|
|
248
|
+
out.push(block.subarray(i + 4, i + len));
|
|
249
|
+
i += len;
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
return [block];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const joined = (records) => Buffer.concat(records.flatMap((r) => [r, Buffer.from([EOR])]));
|
package/lib/kernel/registry.mjs
CHANGED
|
@@ -12,47 +12,56 @@
|
|
|
12
12
|
// literal key `undefined` in every report for several commits. lib/scan.mjs grew an ESETNAME throw
|
|
13
13
|
// to make the next rename fail loudly instead. The throw stays - see `reportKey` below - but the
|
|
14
14
|
// map it guarded is gone, because a name derived from one place cannot disagree with itself.
|
|
15
|
-
import { scan as scanFlow, RULES as FLOW_RULES } from '../sets/flow.mjs';
|
|
16
|
-
import { scanCics, CICS_RULES } from '../sets/cics.mjs';
|
|
17
|
-
import { scanHidden, HIDDEN_RULES } from '../sets/hidden.mjs';
|
|
18
|
-
import { scanCopybooks, COPYBOOK_RULES } from '../sets/copybook.mjs';
|
|
15
|
+
import { scan as scanFlow, scanSteps as scanFlowSteps, RULES as FLOW_RULES } from '../sets/flow.mjs';
|
|
16
|
+
import { scanCics, scanCicsSteps, CICS_RULES } from '../sets/cics.mjs';
|
|
17
|
+
import { scanHidden, scanHiddenSteps, HIDDEN_RULES } from '../sets/hidden.mjs';
|
|
18
|
+
import { scanCopybooks, scanCopybooksSteps, COPYBOOK_RULES } from '../sets/copybook.mjs';
|
|
19
19
|
import { scanJcl, JCL_RULES } from '../sets/jcl.mjs';
|
|
20
20
|
import { scanBuild, BUILD_RULES } from '../sets/build.mjs';
|
|
21
|
-
import { scanRecon, RECON_RULES } from '../sets/recon.mjs';
|
|
21
|
+
import { scanRecon, scanReconSteps, RECON_RULES } from '../sets/recon.mjs';
|
|
22
22
|
import { scanVendor, VENDOR_RULES } from '../sets/vendor.mjs';
|
|
23
23
|
import { scanOpaque, OPAQUE_RULES } from '../sets/opaque.mjs';
|
|
24
|
-
import { scanWeb, WEB_RULES } from '../sets/web.mjs';
|
|
25
|
-
import { scanPriv, PRIV_RULES } from '../sets/priv.mjs';
|
|
26
|
-
import { scanLog, LOG_RULES } from '../sets/log.mjs';
|
|
27
|
-
import { scanCompile, COMPILE_RULES } from '../sets/compile.mjs';
|
|
28
|
-
import { scanSemantics, SEMANTICS_RULES } from '../sets/semantics.mjs';
|
|
24
|
+
import { scanWeb, scanWebSteps, WEB_RULES } from '../sets/web.mjs';
|
|
25
|
+
import { scanPriv, scanPrivSteps, PRIV_RULES } from '../sets/priv.mjs';
|
|
26
|
+
import { scanLog, scanLogSteps, LOG_RULES } from '../sets/log.mjs';
|
|
27
|
+
import { scanCompile, scanCompileSteps, COMPILE_RULES } from '../sets/compile.mjs';
|
|
28
|
+
import { scanSemantics, scanSemanticsSteps, 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
31
|
import { scanHlasm, HLASM_RULES } from '../sets/hlasm.mjs';
|
|
32
|
-
import {
|
|
32
|
+
import { scanIms, IMS_RULES } from '../sets/ims.mjs';
|
|
33
|
+
import { scanDb2, DB2_RULES } from '../sets/ddl.mjs';
|
|
34
|
+
import { scanPli, PLI_RULES } from '../sets/pli.mjs';
|
|
35
|
+
import { scanCrypto, scanCryptoSteps, CRYPTO_RULES } from '../sets/crypto.mjs';
|
|
36
|
+
import { scanSecrets, scanSecretsSteps, SECRETS_RULES } from '../sets/secrets.mjs';
|
|
33
37
|
import { toolName } from './ruleset.mjs';
|
|
34
38
|
|
|
35
39
|
// The order is the order a scan runs them and the order --only lists them. It is not significant
|
|
36
|
-
// to correctness; it is significant to a report being comparable with yesterday's.
|
|
40
|
+
// to correctness; it is significant to a report being comparable with yesterday's. A set with
|
|
41
|
+
// `steps` reads programs through the scan's one shared pass (lib/kernel/shared-pass.mjs).
|
|
37
42
|
export const REGISTRY = [
|
|
38
|
-
{ name: 'flow', scan: scanFlow, rules: FLOW_RULES },
|
|
39
|
-
{ name: 'cics', scan: scanCics, rules: CICS_RULES },
|
|
40
|
-
{ name: 'hidden', scan: scanHidden, rules: HIDDEN_RULES },
|
|
41
|
-
{ name: 'copybook', scan: scanCopybooks, rules: COPYBOOK_RULES },
|
|
43
|
+
{ name: 'flow', scan: scanFlow, steps: scanFlowSteps, rules: FLOW_RULES },
|
|
44
|
+
{ name: 'cics', scan: scanCics, steps: scanCicsSteps, rules: CICS_RULES },
|
|
45
|
+
{ name: 'hidden', scan: scanHidden, steps: scanHiddenSteps, rules: HIDDEN_RULES },
|
|
46
|
+
{ name: 'copybook', scan: scanCopybooks, steps: scanCopybooksSteps, rules: COPYBOOK_RULES },
|
|
42
47
|
{ name: 'jcl', scan: scanJcl, rules: JCL_RULES },
|
|
43
48
|
{ name: 'build', scan: scanBuild, rules: BUILD_RULES },
|
|
44
|
-
{ name: 'recon', scan: scanRecon, rules: RECON_RULES },
|
|
49
|
+
{ name: 'recon', scan: scanRecon, steps: scanReconSteps, rules: RECON_RULES },
|
|
45
50
|
{ name: 'vendor', scan: scanVendor, rules: VENDOR_RULES },
|
|
46
51
|
{ name: 'opaque', scan: scanOpaque, rules: OPAQUE_RULES },
|
|
47
|
-
{ name: 'web', scan: scanWeb, rules: WEB_RULES },
|
|
48
|
-
{ name: 'compile', scan: scanCompile, rules: COMPILE_RULES },
|
|
49
|
-
{ name: 'priv', scan: scanPriv, rules: PRIV_RULES },
|
|
50
|
-
{ name: 'log', scan: scanLog, rules: LOG_RULES },
|
|
51
|
-
{ name: 'semantics', scan: scanSemantics, rules: SEMANTICS_RULES },
|
|
52
|
+
{ name: 'web', scan: scanWeb, steps: scanWebSteps, rules: WEB_RULES },
|
|
53
|
+
{ name: 'compile', scan: scanCompile, steps: scanCompileSteps, rules: COMPILE_RULES },
|
|
54
|
+
{ name: 'priv', scan: scanPriv, steps: scanPrivSteps, rules: PRIV_RULES },
|
|
55
|
+
{ name: 'log', scan: scanLog, steps: scanLogSteps, rules: LOG_RULES },
|
|
56
|
+
{ name: 'semantics', scan: scanSemantics, steps: scanSemanticsSteps, rules: SEMANTICS_RULES },
|
|
52
57
|
{ name: 'zowe', scan: scanZowe, rules: ZOWE_RULES },
|
|
53
58
|
{ name: 'abend', scan: scanAbend, rules: ABEND_RULES },
|
|
54
59
|
{ name: 'hlasm', scan: scanHlasm, rules: HLASM_RULES },
|
|
55
|
-
{ name: '
|
|
60
|
+
{ name: 'ims', scan: scanIms, rules: IMS_RULES },
|
|
61
|
+
{ name: 'ddl', scan: scanDb2, rules: DB2_RULES },
|
|
62
|
+
{ name: 'pli', scan: scanPli, rules: PLI_RULES },
|
|
63
|
+
{ name: 'crypto', scan: scanCrypto, steps: scanCryptoSteps, rules: CRYPTO_RULES },
|
|
64
|
+
{ name: 'secrets', scan: scanSecrets, steps: scanSecretsSteps, rules: SECRETS_RULES },
|
|
56
65
|
];
|
|
57
66
|
|
|
58
67
|
// Re-exported from the kernel's ruleset module, which is where it has to live: this file imports
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// One pass over the tree for every rule set that reads programs, so that a file is read and parsed
|
|
3
|
+
// once per scan rather than once per set.
|
|
4
|
+
//
|
|
5
|
+
// A set's scan is a generator. Where it would run its guarded loop it yields `loopOver(...)` and is
|
|
6
|
+
// resumed with the loop's account, exactly what eachWithinMemory would have returned. `drive` runs
|
|
7
|
+
// one set on its own, its loop as it always ran. `driveTogether` runs several: it gathers the loop
|
|
8
|
+
// each asks for, walks the files of all of them once in the tree's order, gives each set its own
|
|
9
|
+
// files in its own order, and resumes each with its own account. While a file is being handed round,
|
|
10
|
+
// the tree answers text() and parse() for it from what the first set to ask was given.
|
|
11
|
+
//
|
|
12
|
+
// Each set's per-file work is unchanged, so its findings are the ones it reported alone. What is
|
|
13
|
+
// shared is the reading and the parse, which is why no set may change a parse result it is given.
|
|
14
|
+
import { eachWithinMemory, memoryStatus, stoppedBecause, watchMemoryBuffer, MEMORY_RESERVE } from './memory.mjs';
|
|
15
|
+
|
|
16
|
+
const LOOP = Symbol('loop');
|
|
17
|
+
const LOOPS = Symbol('loops');
|
|
18
|
+
|
|
19
|
+
// What a set yields in place of `eachWithinMemory(items, work, opts)`. `guarded: false` asks for a
|
|
20
|
+
// plain loop: every item, no memory check and no byte budget.
|
|
21
|
+
export const loopOver = (items, work, opts = {}) => ({ [LOOP]: true, items, work, opts });
|
|
22
|
+
|
|
23
|
+
// Several loops of one set walked in the same pass, each with its own account, the set resumed with
|
|
24
|
+
// their accounts in the order given. A file both loops hold goes to the first, then the second.
|
|
25
|
+
export const loopsOver = (...loops) => ({ [LOOPS]: true, loops });
|
|
26
|
+
const loopsOf = (value) => (value[LOOPS] ? value.loops : [value]);
|
|
27
|
+
const resumeWith = (value, runs) => (value[LOOPS] ? runs : runs[0]);
|
|
28
|
+
|
|
29
|
+
function plainLoop(items, work, label) {
|
|
30
|
+
for (let i = 0; i < items.length; i++) work(items[i], i);
|
|
31
|
+
return { label, processed: items.length, bytes: 0, skipped: [], complete: true, stoppedBy: null, peakHeapBytes: 0, note: null };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const runAlone = (req) => (req.opts.guarded === false
|
|
35
|
+
? plainLoop(req.items, req.work, req.opts.label || 'scan')
|
|
36
|
+
: eachWithinMemory(req.items, req.work, req.opts));
|
|
37
|
+
|
|
38
|
+
export function drive(steps) {
|
|
39
|
+
let step = steps.next();
|
|
40
|
+
while (!step.done) step = steps.next(resumeWith(step.value, loopsOf(step.value).map(runAlone)));
|
|
41
|
+
return step.value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A tree that, while `focus` names a file, answers bytes(), text() and parse() for that file once and
|
|
45
|
+
// hands every later caller the same answer, its error included. A tree whose text is its bytes
|
|
46
|
+
// through `decode` has the file read once for both.
|
|
47
|
+
export function sharingTree(base) {
|
|
48
|
+
let focus = null;
|
|
49
|
+
let bytes = null;
|
|
50
|
+
let text = null;
|
|
51
|
+
let parsed = null;
|
|
52
|
+
const once = (held, read) => {
|
|
53
|
+
if (!held) { try { held = { value: read() }; } catch (e) { held = { error: e }; } }
|
|
54
|
+
return held;
|
|
55
|
+
};
|
|
56
|
+
const answer = (held) => {
|
|
57
|
+
if (held.error) throw held.error;
|
|
58
|
+
return held.value;
|
|
59
|
+
};
|
|
60
|
+
const tree = {
|
|
61
|
+
...base,
|
|
62
|
+
focusOn(f) { focus = f; bytes = null; text = null; parsed = null; },
|
|
63
|
+
bytes(p) {
|
|
64
|
+
if (p !== focus) return base.bytes(p);
|
|
65
|
+
bytes = once(bytes, () => base.bytes(p));
|
|
66
|
+
return answer(bytes);
|
|
67
|
+
},
|
|
68
|
+
text(p) {
|
|
69
|
+
if (p !== focus) return base.text(p);
|
|
70
|
+
text = once(text, () => (base.decode ? base.decode(tree.bytes(p)) : base.text(p)));
|
|
71
|
+
return answer(text);
|
|
72
|
+
},
|
|
73
|
+
parse(p, src) {
|
|
74
|
+
if (p !== focus) return base.parse(p, src);
|
|
75
|
+
const own = src ?? tree.text(p).text;
|
|
76
|
+
if (!parsed || parsed.src !== own) {
|
|
77
|
+
try { parsed = { src: own, value: base.parse(p, own) }; } catch (e) { parsed = { src: own, error: e }; }
|
|
78
|
+
}
|
|
79
|
+
if (parsed.error) throw parsed.error;
|
|
80
|
+
return parsed.value;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return tree;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Runs every generator to completion and returns their results in the order given. A generator
|
|
87
|
+
// that throws, before its loop or inside it, yields { error } in its place, as a set that throws
|
|
88
|
+
// alone does; the others carry on.
|
|
89
|
+
export function driveTogether(stepsList, tree) {
|
|
90
|
+
const live = stepsList.map((steps) => ({ steps, step: null, result: undefined, error: null }));
|
|
91
|
+
const advance = (g, input, thrown) => {
|
|
92
|
+
try { g.step = thrown ? g.steps.throw(thrown) : g.steps.next(input); } catch (e) { g.error = e; g.step = { done: true }; return; }
|
|
93
|
+
if (g.step.done) g.result = g.step.value;
|
|
94
|
+
};
|
|
95
|
+
for (const g of live) advance(g);
|
|
96
|
+
for (;;) {
|
|
97
|
+
const waiting = live.filter((g) => !g.step.done);
|
|
98
|
+
if (!waiting.length) break;
|
|
99
|
+
const runs = mergedLoop(waiting.flatMap((g) => loopsOf(g.step.value)), tree);
|
|
100
|
+
let at = 0;
|
|
101
|
+
for (const g of waiting) {
|
|
102
|
+
const own = runs.slice(at, at += loopsOf(g.step.value).length);
|
|
103
|
+
advance(g, resumeWith(g.step.value, own.map((r) => r.run)), own.find((r) => r.error)?.error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return live.map((g) => (g.error ? { error: g.error } : g.result));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The files of every request, once each, in the order of the tree's list. A request whose items are
|
|
110
|
+
// not in that order runs alone first, so no set sees its files in an order its own loop would not use.
|
|
111
|
+
function mergedLoop(requests, tree) {
|
|
112
|
+
const rank = new Map(tree.list().map((p, i) => [p, i]));
|
|
113
|
+
const inOrder = (items) => items.every((p, i) => rank.has(p) && (i === 0 || rank.get(items[i - 1]) < rank.get(p)));
|
|
114
|
+
const states = requests.map((req) => ({
|
|
115
|
+
req, guarded: req.opts.guarded !== false, maxBytes: req.opts.maxBytes ?? Infinity, label: req.opts.label || 'scan',
|
|
116
|
+
next: 0, processed: 0, bytes: 0, skipped: [], stoppedBy: null, error: null, alone: null,
|
|
117
|
+
}));
|
|
118
|
+
for (const s of states) if (!inOrder(s.req.items)) {
|
|
119
|
+
try { s.alone = runAlone(s.req); } catch (e) { s.error = e; s.alone = {}; }
|
|
120
|
+
}
|
|
121
|
+
const shared = states.filter((s) => !s.alone);
|
|
122
|
+
const w = shared.map((s) => s.req.opts.watcher).find(Boolean) || watchMemoryBuffer();
|
|
123
|
+
const every = Math.min(...shared.map((s) => s.req.opts.every ?? 8), 8);
|
|
124
|
+
const files = [...new Set(shared.flatMap((s) => s.req.items))].sort((a, b) => rank.get(a) - rank.get(b));
|
|
125
|
+
let stoppedBy = null;
|
|
126
|
+
for (let i = 0; i < files.length; i++) {
|
|
127
|
+
const f = files[i];
|
|
128
|
+
const wanting = shared.filter((s) => s.req.items[s.next] === f);
|
|
129
|
+
if (!stoppedBy && wanting.some((s) => s.guarded && !s.error && s.bytes < s.maxBytes)
|
|
130
|
+
&& (i % every === 0 || memoryStatus().fractionUsed > 1 - MEMORY_RESERVE)) {
|
|
131
|
+
const state = w.check();
|
|
132
|
+
if ((state === 'exhausted' || state === 'tight') && (!w.collect() || w.stillTight())) {
|
|
133
|
+
stoppedBy = state === 'exhausted' ? 'memory exhausted' : 'memory reserve';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
tree.focusOn?.(f);
|
|
137
|
+
for (const s of wanting) {
|
|
138
|
+
const index = s.next++;
|
|
139
|
+
if (s.error) continue;
|
|
140
|
+
if (s.guarded) {
|
|
141
|
+
if (s.stoppedBy) { s.skipped.push(f); continue; }
|
|
142
|
+
if (s.bytes >= s.maxBytes) { s.stoppedBy = 'byte budget'; s.skipped.push(f); continue; }
|
|
143
|
+
if (stoppedBy) { s.stoppedBy = stoppedBy; s.skipped.push(f); continue; }
|
|
144
|
+
}
|
|
145
|
+
let took;
|
|
146
|
+
try { took = s.req.work(f, index); } catch (e) { s.error = e; continue; }
|
|
147
|
+
if (typeof took === 'number') s.bytes += took;
|
|
148
|
+
s.processed++;
|
|
149
|
+
}
|
|
150
|
+
tree.focusOn?.(null);
|
|
151
|
+
}
|
|
152
|
+
return states.map((s) => {
|
|
153
|
+
if (s.alone) return { run: s.alone, error: s.error };
|
|
154
|
+
const { items } = s.req;
|
|
155
|
+
const run = s.guarded
|
|
156
|
+
? { label: s.label, processed: s.processed, bytes: s.bytes, skipped: s.skipped, complete: s.skipped.length === 0, stoppedBy: s.stoppedBy, peakHeapBytes: w.peak,
|
|
157
|
+
note: s.skipped.length === 0 ? null
|
|
158
|
+
: `${s.label}: ${s.skipped.length} of ${items.length} files were not read because ${stoppedBecause(s.stoppedBy)}. `
|
|
159
|
+
+ 'The findings below are over what was read, and are not a result for this repository as a whole.' }
|
|
160
|
+
: { label: s.label, processed: s.processed, bytes: 0, skipped: [], complete: true, stoppedBy: null, peakHeapBytes: 0, note: null };
|
|
161
|
+
return { run, error: s.error };
|
|
162
|
+
});
|
|
163
|
+
}
|