@portll/cobolwork 0.6.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 +49 -18
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +30 -7
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +108 -53
- package/lib/capabilities.mjs +9 -10
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +13 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +52 -3
- package/lib/dataflow.mjs +176 -125
- 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/evidence/cli.mjs +14 -3
- package/lib/evidence/record.mjs +1 -1
- package/lib/evidence/store.mjs +44 -31
- 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 +13 -1
- package/lib/hlasm/asm/sections.mjs +3 -1
- package/lib/hlasm/instr.mjs +25 -0
- package/lib/hlasm/macro/authorization.mjs +24 -4
- package/lib/hlasm/macro/datasets.mjs +30 -17
- package/lib/hlasm/macro/io.mjs +68 -45
- package/lib/hlasm/macro/le.mjs +2 -2
- package/lib/hlasm/macro/operator.mjs +32 -21
- package/lib/hlasm/macro/program.mjs +126 -84
- package/lib/hlasm/macro/recovery.mjs +10 -5
- package/lib/hlasm/macro/storage.mjs +64 -14
- package/lib/hlasm/macro/structured.mjs +1 -1
- package/lib/hlasm/model.mjs +35 -10
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +10 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +22 -8
- 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/ironwork.mjs +17 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +14 -8
- package/lib/kernel/shared-pass.mjs +34 -9
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- 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/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +7 -3
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +41 -3
- package/lib/scan.mjs +7 -1
- package/lib/sets/abend.mjs +16 -6
- package/lib/sets/cics.mjs +15 -35
- package/lib/sets/compile.mjs +41 -15
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +18 -1
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +72 -12
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +6 -6
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +5 -3
- package/lib/sets/semantics.mjs +3 -0
- package/lib/sets/web.mjs +36 -22
- 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 +432 -1
- package/rules/compliance-dora.json +414 -1
- package/rules/compliance-ffiec.json +414 -1
- package/rules/compliance-nist80053.json +466 -1
- 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.site.schema.json +116 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// The compile check `cobolwork build --precompile` runs before the caller's compiler: each program the
|
|
3
|
+
// compiler command names that holds EXEC SQL or EXEC CICS is translated by lib/precompile.mjs into a
|
|
4
|
+
// directory of its own, beside the stand-in copybooks and translated members it needs, and given to
|
|
5
|
+
// the same compiler with -fsyntax-only, so a program GnuCOBOL refuses only for its EXEC blocks is
|
|
6
|
+
// checked. Nothing it writes outlives the check.
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { basename, delimiter, dirname, extname, join, relative, resolve } from 'node:path';
|
|
11
|
+
import { parseBms } from './bms.mjs';
|
|
12
|
+
import { printable } from './kernel/printable.mjs';
|
|
13
|
+
import { detectFormat, parseSource } from './parser.mjs';
|
|
14
|
+
import { precompile } from './precompile.mjs';
|
|
15
|
+
import { isBms, isProgram, readSource } from './sources.mjs';
|
|
16
|
+
|
|
17
|
+
const COPY_EXTENSIONS = ['', '.cpy', '.CPY', '.cbl', '.CBL', '.cob', '.COB'];
|
|
18
|
+
const MAX_ERRORS = 3;
|
|
19
|
+
|
|
20
|
+
// A COPY member's text from the first directory holding it, as cobc searches them.
|
|
21
|
+
export const copybookIn = (dirs) => (name) => {
|
|
22
|
+
for (const d of dirs) {
|
|
23
|
+
for (const ext of COPY_EXTENSIONS) {
|
|
24
|
+
const p = join(d, name + ext);
|
|
25
|
+
try { if (statSync(p).isFile()) return readSource(p).text; } catch { /* not here */ }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// The BMS files under a repository, the member names its other files supply, and the mapsets it holds
|
|
32
|
+
// no copybook for, whose symbolic maps the translator writes.
|
|
33
|
+
export function repositoryBms(repoRoot) {
|
|
34
|
+
const files = [];
|
|
35
|
+
const walk = (d) => {
|
|
36
|
+
let es;
|
|
37
|
+
try { es = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
38
|
+
for (const e of es) {
|
|
39
|
+
if (e.name === '.git' || e.name.startsWith('._')) continue;
|
|
40
|
+
const p = join(d, e.name);
|
|
41
|
+
if (e.isDirectory()) walk(p); else files.push(p);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
walk(repoRoot);
|
|
45
|
+
const bms = files.filter(isBms);
|
|
46
|
+
const members = new Set(files.filter((f) => !bms.includes(f)).map((f) => basename(f, extname(f)).toUpperCase()));
|
|
47
|
+
const mapsets = bms.flatMap((f) => { try { return parseBms(readFileSync(f, 'latin1')).mapsets; } catch { return []; } })
|
|
48
|
+
.filter((ms) => ms.name && !members.has(ms.name.toUpperCase()));
|
|
49
|
+
return { bms, has: (n) => members.has(n.toUpperCase()), mapsets };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const EXEC = /EXEC\s+(?:SQL|CICS)\b/i;
|
|
53
|
+
const DLI = /EXEC\s+DLI\b/i;
|
|
54
|
+
const redact = (s) => String(s).replace(/'[^']*'|"[^"]*"/g, "'…'");
|
|
55
|
+
|
|
56
|
+
// The copy directories a cobc or gcobol argument vector names, in order: -I <dir> and -I<dir>.
|
|
57
|
+
function includeDirsOf(argv, cwd) {
|
|
58
|
+
const dirs = [];
|
|
59
|
+
argv.forEach((a, i) => {
|
|
60
|
+
if (a === '-I' && argv[i + 1]) dirs.push(resolve(cwd, argv[i + 1]));
|
|
61
|
+
else if (a.startsWith('-I') && a.length > 2) dirs.push(resolve(cwd, a.slice(2)));
|
|
62
|
+
});
|
|
63
|
+
return dirs;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The argument vector with -o and its value taken out: a syntax check writes nothing.
|
|
67
|
+
const withoutOutput = (argv) => argv.filter((a, i) => a !== '-o' && argv[i - 1] !== '-o' && !(a.startsWith('-o') && a.length > 2));
|
|
68
|
+
|
|
69
|
+
// A compiler's first error lines, each named by the source line it came from and with every quoted
|
|
70
|
+
// literal masked, since a diagnostic may quote the program and no output of the gate carries source.
|
|
71
|
+
function errorsOf(stderr, translated, map, rel) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const line of String(stderr || '').split('\n')) {
|
|
74
|
+
const m = /^(.*?):(\d+):\s*error:\s*(.*)$/.exec(line);
|
|
75
|
+
if (!m) continue;
|
|
76
|
+
const at = resolve(m[1]) === translated ? `${rel}:${map[Number(m[2]) - 1] ?? m[2]}` : `${basename(m[1])}:${m[2]}`;
|
|
77
|
+
out.push(printable(`${at}: ${redact(m[3])}`, 300));
|
|
78
|
+
if (out.length === MAX_ERRORS) break;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Runs the check. `compilerPath` is the resolved compiler and `argv` the caller's arguments after it;
|
|
84
|
+
// `cwd` is where the caller's compiler runs, so its relative paths resolve there. Returns, for each
|
|
85
|
+
// program the argument vector names that holds EXEC SQL or CICS, whether the compiler accepted its
|
|
86
|
+
// translation and its first errors; programs holding EXEC DLI, which nothing translates, are skipped.
|
|
87
|
+
export function precompileCheck({ compilerPath, argv, cwd = process.cwd(), repo, copylibs = [], env = process.env }) {
|
|
88
|
+
const sources = argv.map((a, i) => ({ a, i, path: resolve(cwd, a) }))
|
|
89
|
+
.filter(({ a, i, path }) => !a.startsWith('-') && argv[i - 1] !== '-o' && argv[i - 1] !== '-I' && isProgram(path) && isFile(path));
|
|
90
|
+
const includeDirs = [...includeDirsOf(argv, cwd), ...String(env.COBCPY || '').split(delimiter).filter(Boolean), ...copylibs];
|
|
91
|
+
const others = withoutOutput(argv.filter((_, i) => !sources.some((s) => s.i === i)));
|
|
92
|
+
const programs = [];
|
|
93
|
+
const skipped = [];
|
|
94
|
+
let mapsets = null;
|
|
95
|
+
const root = mkdtempSync(join(tmpdir(), 'cw-precompile-'));
|
|
96
|
+
try {
|
|
97
|
+
for (const { path } of sources) {
|
|
98
|
+
const rel = printable(relative(repo, path).startsWith('..') ? relative(cwd, path) : relative(repo, path), 200);
|
|
99
|
+
const text = readSource(path).text;
|
|
100
|
+
if (DLI.test(text)) { skipped.push({ path: rel, why: 'EXEC DLI is not translated' }); continue; }
|
|
101
|
+
if (!EXEC.test(text)) continue;
|
|
102
|
+
const format = detectFormat(text);
|
|
103
|
+
const dirs = [dirname(path), ...includeDirs];
|
|
104
|
+
let items = [];
|
|
105
|
+
try { items = parseSource(text, path, { format, includeDirs: dirs }).programs.flatMap((p) => p.items); } catch { /* unparsed: no host-variable arrays known */ }
|
|
106
|
+
if (/EXEC\s+CICS\b/i.test(text) && !mapsets) mapsets = repositoryBms(repo).mapsets;
|
|
107
|
+
const p = precompile(text, { format, items, mapsets: /EXEC\s+CICS\b/i.test(text) ? mapsets : [], copybook: copybookIn(dirs) });
|
|
108
|
+
const dir = mkdtempSync(join(root, 'p-'));
|
|
109
|
+
const translated = join(dir, basename(path));
|
|
110
|
+
writeFileSync(translated, p.text);
|
|
111
|
+
for (const [name, body] of Object.entries(p.copybooks)) writeFileSync(join(dir, `${name}.cpy`), body);
|
|
112
|
+
const r = spawnSync(compilerPath, ['-fsyntax-only', '-I', dir, ...others, translated], { cwd, env, encoding: 'utf8', windowsHide: true });
|
|
113
|
+
const ok = !r.error && r.status === 0;
|
|
114
|
+
programs.push({ path: rel, ok, ...(ok ? {} : { errors: r.error ? [printable(r.error.message, 200)] : errorsOf(r.stderr, translated, p.map, rel) }) });
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
rmSync(root, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
return { argv: ['-fsyntax-only', '-I', '<translation>', ...others, '<program>'], programs, skipped };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isFile(path) {
|
|
123
|
+
try { return statSync(path).isFile(); } catch { return false; }
|
|
124
|
+
}
|
package/lib/precompile-cics.mjs
CHANGED
|
@@ -39,13 +39,17 @@ export function cicsOptions(text) {
|
|
|
39
39
|
return out;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Which table entry a command is: the one
|
|
43
|
-
//
|
|
42
|
+
// Which table entry a command is: the longest one whose name the words after the verb spell in order
|
|
43
|
+
// (WEB READ HTTPHEADER; INQUIRE TRANSACTION though a PROGRAM option follows), else the one an
|
|
44
|
+
// option word identifies (SEND MAP by MAP), else the verb alone, else the one IBM makes the default
|
|
45
|
+
// (READQ is READQ TS).
|
|
44
46
|
export function cicsCommand(options) {
|
|
45
47
|
const verb = options[0]?.word;
|
|
46
48
|
const words = new Set(options.slice(1).map((o) => o.word));
|
|
47
49
|
const names = Object.keys(CICS_COMMANDS).filter((n) => n === verb || n.startsWith(`${verb} `));
|
|
48
|
-
|
|
50
|
+
const spelled = names.map((n) => n.split(' ')).filter((w) => w.length > 1 && w.slice(1).every((x, i) => options[i + 1]?.word === x))
|
|
51
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
52
|
+
return spelled?.join(' ') || names.find((n) => CICS_COMMANDS[n].identify?.some((w) => words.has(w)))
|
|
49
53
|
|| names.find((n) => n === verb) || names.find((n) => CICS_COMMANDS[n].default) || null;
|
|
50
54
|
}
|
|
51
55
|
|
package/lib/reach.mjs
CHANGED
|
@@ -14,11 +14,15 @@
|
|
|
14
14
|
// lib/exploitability.mjs joins them with the route into a verdict.
|
|
15
15
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
16
16
|
import { basename, sep, delimiter } from 'node:path';
|
|
17
|
+
import { versionProblem, unknownKeys, ignoredKey } from './kernel/version-key.mjs';
|
|
17
18
|
|
|
18
19
|
const str = (v) => typeof v === 'string' && v.length > 0;
|
|
19
20
|
const isoDate = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v);
|
|
20
21
|
const up = (s) => String(s || '').toUpperCase();
|
|
21
22
|
const ACCESS = new Set(['open', 'restricted']);
|
|
23
|
+
export const REACH_VERSION = 1;
|
|
24
|
+
const REACH_KEYS = new Set(['$schema', 'version', 'extract', 'retrieved', 'transactions', 'jobs', 'privileged']);
|
|
25
|
+
const PRIVILEGED_KEYS = new Set(['transactions', 'jobs']);
|
|
22
26
|
const arr = (v) => (Array.isArray(v) ? v.filter(str).map(up) : []);
|
|
23
27
|
|
|
24
28
|
// The feeds a scan was given: the caller's list, or COBOLWORK_REACH read when the scan runs.
|
|
@@ -34,7 +38,7 @@ export function reachFeedPaths(opts = {}) {
|
|
|
34
38
|
// derived from a RACF unload (who may start each transaction, and which run with elevated authority)
|
|
35
39
|
// and the SIT (which face a network).
|
|
36
40
|
export function loadReachFeed(path, { root = null } = {}) {
|
|
37
|
-
const feed = { file: basename(path), extract: null, retrieved: null, transactions: {}, jobs: {}, privileged: { transactions: [], jobs: [] }, refused: [], problem: null };
|
|
41
|
+
const feed = { file: basename(path), extract: null, retrieved: null, transactions: {}, jobs: {}, privileged: { transactions: [], jobs: [] }, refused: [], warnings: [], problem: null };
|
|
38
42
|
let real;
|
|
39
43
|
try { real = realpathSync(path); } catch (e) { feed.problem = `could not be read (${e.code || e.name})`; return feed; }
|
|
40
44
|
if (root) {
|
|
@@ -43,6 +47,9 @@ export function loadReachFeed(path, { root = null } = {}) {
|
|
|
43
47
|
}
|
|
44
48
|
let doc;
|
|
45
49
|
try { doc = JSON.parse(readFileSync(real, 'utf8')); } catch (e) { feed.problem = `is not JSON (${e.message})`; return feed; }
|
|
50
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) { feed.problem = 'is not a JSON object'; return feed; }
|
|
51
|
+
feed.problem = versionProblem(doc, REACH_VERSION);
|
|
52
|
+
if (feed.problem) return feed;
|
|
46
53
|
if (!str(doc.extract)) { feed.problem = 'does not say which extract it is'; return feed; }
|
|
47
54
|
if (!isoDate(doc.retrieved)) { feed.problem = 'does not say when it was retrieved'; return feed; }
|
|
48
55
|
feed.extract = doc.extract;
|
|
@@ -56,7 +63,9 @@ export function loadReachFeed(path, { root = null } = {}) {
|
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
const pv = doc.privileged;
|
|
59
|
-
|
|
66
|
+
const held = pv && typeof pv === 'object' && !Array.isArray(pv);
|
|
67
|
+
if (held) { feed.privileged.transactions = arr(pv.transactions); feed.privileged.jobs = arr(pv.jobs); }
|
|
68
|
+
feed.warnings = [...unknownKeys(doc, REACH_KEYS).map((k) => ignoredKey(feed.file, k)), ...(held ? unknownKeys(pv, PRIVILEGED_KEYS) : []).map((k) => ignoredKey(`${feed.file} privileged`, k))];
|
|
60
69
|
return feed;
|
|
61
70
|
}
|
|
62
71
|
|
package/lib/revision.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"4d7151cd03c0ac0850eec10a2529b07f0fc04aa1","tag":"v0.7.0"}
|
package/lib/sarif.mjs
CHANGED
|
@@ -45,13 +45,23 @@ const DECLARED = new Map([...Object.entries(ALL_RULES), ...Object.entries(DIFF_R
|
|
|
45
45
|
const SET_OF = new Map();
|
|
46
46
|
for (const s of REGISTRY) for (const id of Object.keys(s.rules)) SET_OF.set(id, s.name);
|
|
47
47
|
|
|
48
|
+
// A related entry without a path is an advisory the finding rests on, not a place in the tree.
|
|
49
|
+
const located = (f) => (f.related || []).filter((r) => r.path);
|
|
50
|
+
|
|
51
|
+
const cweTaxon = (cwe) => /^CWE-(\d+)$/.exec(cwe || '')?.[1] ?? null;
|
|
52
|
+
|
|
53
|
+
// The CWE taxonomy is run.taxonomies[0] whenever any rule names a CWE.
|
|
54
|
+
const CWE_COMPONENT = { name: 'CWE', index: 0 };
|
|
55
|
+
|
|
48
56
|
// SARIF's level cannot say what kind of claim a result is, so evidence rides in the property bags.
|
|
49
57
|
const descriptorFor = (report) => (id) => {
|
|
50
58
|
const properties = {
|
|
51
59
|
...(report.ruleCwe?.[id] ? { cwe: report.ruleCwe[id] } : {}),
|
|
52
60
|
...(report.ruleEvidence?.[id] ? { evidence: report.ruleEvidence[id] } : {}),
|
|
61
|
+
...(report.ruleCompliance?.[id] ? { compliance: report.ruleCompliance[id] } : {}),
|
|
53
62
|
...(SECURITY_SEVERITY[DECLARED.get(id)] ? { 'security-severity': SECURITY_SEVERITY[DECLARED.get(id)] } : {}),
|
|
54
63
|
};
|
|
64
|
+
const taxon = cweTaxon(report.ruleCwe?.[id]);
|
|
55
65
|
return {
|
|
56
66
|
id,
|
|
57
67
|
shortDescription: { text: report.ruleText?.[id] || id },
|
|
@@ -59,10 +69,23 @@ const descriptorFor = (report) => (id) => {
|
|
|
59
69
|
// rule. `help` is where GitHub code scanning shows a recommendation.
|
|
60
70
|
...(report.ruleImpact?.[id] ? { fullDescription: { text: textOf(report.ruleImpact[id]) } } : {}),
|
|
61
71
|
...(report.ruleRemedy?.[id] ? { help: { text: textOf(report.ruleRemedy[id]) } } : {}),
|
|
72
|
+
...(taxon ? { relationships: [{ target: { id: taxon, toolComponent: CWE_COMPONENT } }] } : {}),
|
|
62
73
|
...(Object.keys(properties).length ? { properties } : {}),
|
|
63
74
|
};
|
|
64
75
|
};
|
|
65
76
|
|
|
77
|
+
function cweTaxonomy(report, ruleIds) {
|
|
78
|
+
const taxa = [...new Set(ruleIds.map((id) => cweTaxon(report.ruleCwe?.[id])).filter(Boolean))].sort((a, b) => a - b);
|
|
79
|
+
if (!taxa.length) return [];
|
|
80
|
+
return [{
|
|
81
|
+
name: CWE_COMPONENT.name,
|
|
82
|
+
organization: 'MITRE',
|
|
83
|
+
shortDescription: { text: 'Common Weakness Enumeration' },
|
|
84
|
+
informationUri: 'https://cwe.mitre.org/',
|
|
85
|
+
taxa: taxa.map((id) => ({ id, helpUri: `https://cwe.mitre.org/data/definitions/${id}.html` })),
|
|
86
|
+
}];
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
function notifications(report) {
|
|
67
90
|
const bySet = report.summary?.bySet || {};
|
|
68
91
|
const execution = [];
|
|
@@ -111,6 +134,9 @@ function notifications(report) {
|
|
|
111
134
|
properties: { set },
|
|
112
135
|
});
|
|
113
136
|
}
|
|
137
|
+
for (const [id, warnings] of [['cobolwork/site-key-ignored', report.summary?.siteWarnings], ['cobolwork/feed-key-ignored', report.summary?.feedWarnings]]) {
|
|
138
|
+
for (const w of warnings || []) configuration.push({ descriptor: { id }, level: 'warning', message: { text: textOf(w) } });
|
|
139
|
+
}
|
|
114
140
|
// A diff whose two sides were scanned under different configuration.
|
|
115
141
|
for (const { file, change } of report.summary?.configurationChanged || []) {
|
|
116
142
|
configuration.push({
|
|
@@ -144,6 +170,16 @@ export function toSarif(report, { toolVersion = '0.1.0' } = {}) {
|
|
|
144
170
|
|
|
145
171
|
const { execution, configuration } = notifications(report);
|
|
146
172
|
const s = report.summary || {};
|
|
173
|
+
const taxonomies = cweTaxonomy(report, present);
|
|
174
|
+
|
|
175
|
+
// Every file a result or related location names, each listed once; locations point at it by index.
|
|
176
|
+
const results = [...report.findings, ...suppressed];
|
|
177
|
+
const uris = [...new Set(results.flatMap((f) => [f.path, ...located(f).map((r) => r.path)]).map(uriOf))].sort();
|
|
178
|
+
const artifactIndex = new Map(uris.map((uri, i) => [uri, i]));
|
|
179
|
+
const artifactLocation = (path) => {
|
|
180
|
+
const uri = uriOf(path);
|
|
181
|
+
return { uri, uriBaseId: '%SRCROOT%', index: artifactIndex.get(uri) };
|
|
182
|
+
};
|
|
147
183
|
|
|
148
184
|
return {
|
|
149
185
|
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
@@ -158,6 +194,7 @@ export function toSarif(report, { toolVersion = '0.1.0' } = {}) {
|
|
|
158
194
|
},
|
|
159
195
|
...(extensions.length ? { extensions } : {}),
|
|
160
196
|
},
|
|
197
|
+
...(taxonomies.length ? { taxonomies } : {}),
|
|
161
198
|
invocations: [{
|
|
162
199
|
// The verdict, where a consumer that reads nothing else will still see it.
|
|
163
200
|
executionSuccessful: s.nosrc !== true,
|
|
@@ -183,7 +220,8 @@ export function toSarif(report, { toolVersion = '0.1.0' } = {}) {
|
|
|
183
220
|
},
|
|
184
221
|
},
|
|
185
222
|
}],
|
|
186
|
-
|
|
223
|
+
...(uris.length ? { artifacts: uris.map((uri) => ({ location: { uri, uriBaseId: '%SRCROOT%' } })) } : {}),
|
|
224
|
+
results: results.map((f) => {
|
|
187
225
|
const set = SET_OF.get(f.rule);
|
|
188
226
|
const component = set !== undefined ? indexOfSet.get(set) : undefined;
|
|
189
227
|
const within = component === undefined ? null : extensions[component].rules.findIndex((r) => r.id === f.rule);
|
|
@@ -217,8 +255,8 @@ export function toSarif(report, { toolVersion = '0.1.0' } = {}) {
|
|
|
217
255
|
// a subprogram no caller run is shown to pass.
|
|
218
256
|
...(f.abend?.inputFrom ? { inputFrom: f.abend.inputFrom } : {}),
|
|
219
257
|
} } : {}),
|
|
220
|
-
locations: [{ physicalLocation: { artifactLocation:
|
|
221
|
-
...(f.
|
|
258
|
+
locations: [{ physicalLocation: { artifactLocation: artifactLocation(f.path), region: { startLine: f.line || 1 } } }],
|
|
259
|
+
...(located(f).length ? { relatedLocations: located(f).map((r) => ({ physicalLocation: { artifactLocation: artifactLocation(r.path), region: { startLine: r.line || 1 } }, message: { text: textOf(r.detail) } })) } : {}),
|
|
222
260
|
...(f.suppressed ? { suppressions: [{ kind: 'external', status: 'accepted', justification: `${f.suppressed.action} by ${f.suppressed.who} until ${f.suppressed.expires}: ${f.suppressed.reason}` }] } : {}),
|
|
223
261
|
};
|
|
224
262
|
}),
|
package/lib/scan.mjs
CHANGED
|
@@ -66,6 +66,9 @@ function mergeEstate(reports) {
|
|
|
66
66
|
effectNote: effectless.length ? `no authority facts declared in ${of(effectless)}, so no finding there was judged to run privileged: name privilegedTransactions/privilegedJobs in each repository's cobolwork.site.json, or bring them in the COBOLWORK_REACH extract` : null,
|
|
67
67
|
witnessFeeds: first('witnessFeeds'), byWitness: addUp('byWitness'), witnessFeedProblems: union('witnessFeedProblems'),
|
|
68
68
|
witnessUnmatched: unmatched, witnessOverruled: overruled,
|
|
69
|
+
executionFeeds: first('executionFeeds'), byExecution: addUp('byExecution'), executionFeedProblems: union('executionFeedProblems'),
|
|
70
|
+
siteWarnings: all.flatMap((x) => (x.s.siteWarnings || []).map((w) => `${x.repo}/${w}`)),
|
|
71
|
+
feedWarnings: union('feedWarnings'),
|
|
69
72
|
};
|
|
70
73
|
return Object.fromEntries(Object.entries(merged).filter(([, v]) => (Array.isArray(v) ? v.length : v)));
|
|
71
74
|
}
|
|
@@ -102,7 +105,7 @@ function scanEach(root, opts) {
|
|
|
102
105
|
filesScanned, programs: sum('programs'), programFiles: sum('programFiles'), copybookFiles: sum('copybookFiles'),
|
|
103
106
|
jclFiles: sum('jclFiles'), copiesMissing: sum('copiesMissing'), filesUnreadable: sum('filesUnreadable'),
|
|
104
107
|
filesEbcdic: sum('filesEbcdic'), filesOverBudget: sum('filesOverBudget'), crossProgram: sum('crossProgram'),
|
|
105
|
-
ruleSets: opts.only || RULE_SETS, nosrc: filesScanned === 0,
|
|
108
|
+
ruleSets: opts.only || RULE_SETS, flowModel: FLOW_MODEL, toolVersion: TOOL_VERSION, nosrc: filesScanned === 0,
|
|
106
109
|
coverageIncomplete: reports.some(({ r }) => r.summary.coverageIncomplete),
|
|
107
110
|
identity: { version: FINGERPRINT_VERSION, shared },
|
|
108
111
|
...(reports[0]?.r.summary.advisoryFeeds ? { advisoryFeeds: reports[0].r.summary.advisoryFeeds } : {}),
|
|
@@ -158,7 +161,10 @@ export function applyEstateFacts(findings, checked, root, opts = {}) {
|
|
|
158
161
|
const { byExecution } = applyExecution(findings, root, executionFeeds);
|
|
159
162
|
const driven = byExploitability ? byExploitability['attacker-driven'] : 0;
|
|
160
163
|
const loaded = witnessFeeds.filter((f) => !f.problem);
|
|
164
|
+
const feedWarnings = [...reachFeeds, ...witnessFeeds].flatMap((f) => f.warnings).sort();
|
|
161
165
|
return {
|
|
166
|
+
...(site.warnings.length ? { siteWarnings: site.warnings } : {}),
|
|
167
|
+
...(feedWarnings.length ? { feedWarnings } : {}),
|
|
162
168
|
...(byReach ? { byReach } : {}),
|
|
163
169
|
...(byEffect ? { byEffect } : {}),
|
|
164
170
|
...(reachFeeds.some(f => !f.problem) ? { reachFeeds: reachFeeds.filter(f => !f.problem).map(f => ({ file: f.file, extract: f.extract, retrieved: f.retrieved })) } : {}),
|
package/lib/sets/abend.mjs
CHANGED
|
@@ -63,6 +63,10 @@ const OPTIMIZED = {
|
|
|
63
63
|
// reference modification's start, length, and start plus length.
|
|
64
64
|
const RANGE_MESSAGES = /^IGZ00(06|07|72|73|74)S\b/;
|
|
65
65
|
|
|
66
|
+
// Language Environment's message for a dynamic CALL of a module no library holds, which ironwork
|
|
67
|
+
// ends U4038 (its assumption C450); earlier ironwork releases ended the same CALL S806.
|
|
68
|
+
const MODULE_NOT_FOUND = /^CEE3501S The module .* was not found\.$/;
|
|
69
|
+
|
|
66
70
|
export function abendRunPaths(opts = {}) {
|
|
67
71
|
if (opts.abendRuns) return opts.abendRuns;
|
|
68
72
|
return (process.env.COBOLWORK_ABENDS || '').split(delimiter).filter(Boolean);
|
|
@@ -72,13 +76,19 @@ export function abendRunPaths(opts = {}) {
|
|
|
72
76
|
// code CICS reports as ASRA.
|
|
73
77
|
const CICS_PROGRAM_CHECK = /\((S0C7|S0C4), which CICS reports as ASRA\)$/;
|
|
74
78
|
|
|
79
|
+
// How ironwork ends the message of a Language Environment condition in a CICS task, U4038 in
|
|
80
|
+
// batch, which CICS reports as transaction abend 4038 (its assumption C454).
|
|
81
|
+
const CICS_LE_CONDITION = / \(U4038, which CICS reports as transaction abend 4038\)$/;
|
|
82
|
+
|
|
75
83
|
export function abendRule(abend) {
|
|
76
|
-
const
|
|
84
|
+
const cicsLe = abend.code === '4038' && CICS_LE_CONDITION.test(abend.message || '');
|
|
85
|
+
const message = cicsLe ? abend.message.replace(CICS_LE_CONDITION, '') : abend.message || '';
|
|
86
|
+
const code = cicsLe ? 'U4038' : (abend.code === 'ASRA' && CICS_PROGRAM_CHECK.exec(message)?.[1]) || abend.code;
|
|
77
87
|
if (code === 'S0C7') return 'input-causes-abend-s0c7';
|
|
78
88
|
if (code === 'S0C4') return 'input-causes-abend-s0c4';
|
|
79
89
|
if (code === 'S322') return 'input-causes-hang';
|
|
80
|
-
if (code === 'S806') return 'input-selects-program';
|
|
81
|
-
if (RANGE_MESSAGES.test(
|
|
90
|
+
if (code === 'S806' || (code === 'U4038' && MODULE_NOT_FOUND.test(message))) return 'input-selects-program';
|
|
91
|
+
if (RANGE_MESSAGES.test(message)) return 'input-causes-abend-subscript-range';
|
|
82
92
|
return 'input-causes-abend';
|
|
83
93
|
}
|
|
84
94
|
|
|
@@ -208,10 +218,10 @@ export function loadAbendRun(dir, tree) {
|
|
|
208
218
|
const where = isString(place.file) ? posix.normalize(posix.join(rootDir ?? posix.dirname(programFile), place.file.replace(/\\/g, '/'))) : programFile;
|
|
209
219
|
const line = Number.isInteger(place.line) && place.line > 0 ? place.line : 1;
|
|
210
220
|
const rule = abendRule(run.abend);
|
|
211
|
-
//
|
|
212
|
-
// fuzz put in the input reaching that CALL's program load.
|
|
221
|
+
// A CALL of a missing program is input choosing the program only where the run's own journal
|
|
222
|
+
// records the marker fuzz put in the input reaching that CALL's program load.
|
|
213
223
|
if (rule === 'input-selects-program' && !recorded.sinks.some((k) => k.sink === 'dynamic-program-load' && k.reached === true && k.file === place.file && k.line === place.line)) {
|
|
214
|
-
out.problems.push(`run ${run.journal}: its journal does not record the input reaching the CALL at ${place.file}:${place.line}, so its
|
|
224
|
+
out.problems.push(`run ${run.journal}: its journal does not record the input reaching the CALL at ${place.file}:${place.line}, so its missing-program ending is not reported`);
|
|
215
225
|
continue;
|
|
216
226
|
}
|
|
217
227
|
const used = (Array.isArray(run.input) ? run.input : []).map((id) => inputs.get(id)).filter(Boolean)
|
package/lib/sets/cics.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { parseSource } from '../parser.mjs';
|
|
|
7
7
|
import { drive, loopOver } from '../kernel/shared-pass.mjs';
|
|
8
8
|
import { buildControl, hasFact } from '../control.mjs';
|
|
9
9
|
import { parseCsd } from '../csd.mjs';
|
|
10
|
+
import { execReading } from '../exec-reading.mjs';
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
// `impact` says who can do what once the construct is present, so a reader can tell a defect they
|
|
@@ -113,27 +114,6 @@ export function signOnTest(test) {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
|
|
116
|
-
function execOptions(exec) {
|
|
117
|
-
const opts = new Map();
|
|
118
|
-
const words = [];
|
|
119
|
-
const toks = exec.toks;
|
|
120
|
-
for (let i = 0; i < toks.length; i++) {
|
|
121
|
-
const t = toks[i];
|
|
122
|
-
if (t.t !== 'word') continue;
|
|
123
|
-
words.push(t.u);
|
|
124
|
-
if (toks[i + 1] && toks[i + 1].t === 'sep' && toks[i + 1].v === '(') {
|
|
125
|
-
const inner = [];
|
|
126
|
-
let depth = 1;
|
|
127
|
-
for (let k = i + 2; k < toks.length && depth > 0; k++) {
|
|
128
|
-
if (toks[k].t === 'sep') { depth += toks[k].v === '(' ? 1 : -1; if (depth === 0) break; continue; }
|
|
129
|
-
inner.push(toks[k]);
|
|
130
|
-
}
|
|
131
|
-
if (!opts.has(t.u)) opts.set(t.u, inner);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return { opts, words };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
117
|
const subtreeRead = (x) => x.directRefs > 0 || x.children.some(subtreeRead);
|
|
138
118
|
|
|
139
119
|
// Whether EIBCALEN bounds what the program reads: a condition tests it, or a field it was moved or
|
|
@@ -164,8 +144,8 @@ function testsLength(p) {
|
|
|
164
144
|
// Commands whose failure a program ordinarily lets pass: removing a temporary-storage queue that may
|
|
165
145
|
// not exist, a line written to a transient-data log, the clock, a screen sent to a terminal that may
|
|
166
146
|
// have gone, an ASSIGN, and the RETURN that ends the task anyway.
|
|
167
|
-
const QUIET_FAILURES = (
|
|
168
|
-
|| ['ASKTIME', 'FORMATTIME', 'SEND', 'ASSIGN', 'RETURN'].includes(
|
|
147
|
+
const QUIET_FAILURES = ({ verb, sub }) => (verb === 'DELETEQ' && sub === 'TS') || (verb === 'WRITEQ' && sub === 'TD')
|
|
148
|
+
|| ['ASKTIME', 'FORMATTIME', 'SEND', 'ASSIGN', 'RETURN'].includes(verb);
|
|
169
149
|
const SHOWN_IGNORED = 5;
|
|
170
150
|
|
|
171
151
|
// Commands whose outcome the program cannot see: NOHANDLE with no RESP and no EIBRESP test before the
|
|
@@ -175,15 +155,16 @@ function conditionsIgnored(p, path) {
|
|
|
175
155
|
const execs = p.execs.filter((e) => e.kind === 'CICS').sort((a, b) => a.line - b.line);
|
|
176
156
|
const hits = [];
|
|
177
157
|
execs.forEach((e, k) => {
|
|
178
|
-
const
|
|
179
|
-
|
|
158
|
+
const reading = execReading(e);
|
|
159
|
+
const { opts: o, words, verb, sub } = reading;
|
|
160
|
+
if (verb === 'IGNORE' && sub === 'CONDITION') {
|
|
180
161
|
hits.push({ e, what: `IGNORE CONDITION ${words.slice(2).join(' ')}`.trim() });
|
|
181
162
|
return;
|
|
182
163
|
}
|
|
183
|
-
if (!words.includes('NOHANDLE') || o.has('RESP') || QUIET_FAILURES(
|
|
164
|
+
if (!words.includes('NOHANDLE') || o.has('RESP') || QUIET_FAILURES(reading)) return;
|
|
184
165
|
const until = execs[k + 1] ? execs[k + 1].line : Infinity;
|
|
185
166
|
const tested = p.statements.some((st) => st.line > e.line && st.line < until && (st.sources || []).some((t) => t.t === 'word' && t.u === 'EIBRESP'));
|
|
186
|
-
if (!tested) hits.push({ e, what: `${
|
|
167
|
+
if (!tested) hits.push({ e, what: `${verb}${sub && sub !== 'NOHANDLE' && !o.has(sub) ? ` ${sub}` : ''} NOHANDLE` });
|
|
187
168
|
});
|
|
188
169
|
if (!hits.length) return null;
|
|
189
170
|
const shown = hits.slice(0, SHOWN_IGNORED).map((h) => `${h.what} at line ${h.e.line}`).join(', ');
|
|
@@ -226,8 +207,7 @@ export function* scanCicsSteps(root, opts = {}) {
|
|
|
226
207
|
const transfers = [];
|
|
227
208
|
for (const e of p.execs) {
|
|
228
209
|
if (e.kind !== 'CICS') continue;
|
|
229
|
-
const { opts: o,
|
|
230
|
-
const verb = w[0];
|
|
210
|
+
const { opts: o, verb } = execReading(e);
|
|
231
211
|
if (!['LINK', 'XCTL', 'START'].includes(verb)) continue;
|
|
232
212
|
const pgm = (o.get('PROGRAM') || o.get('TRANSID') || [])[0];
|
|
233
213
|
const areaTok = (o.get('COMMAREA') || o.get('FROM') || [])[0];
|
|
@@ -275,7 +255,7 @@ export function* scanCicsSteps(root, opts = {}) {
|
|
|
275
255
|
// asking the security manager, and one that folds a password's case first. Passwords are recognised
|
|
276
256
|
// by field name, as the sign-on rules above recognise them, and each finding says so.
|
|
277
257
|
function passwordChecks(p, path) {
|
|
278
|
-
if (p.execs.some((e) => e.kind === 'CICS' && /^(VERIFY|SIGNON)$/.test(
|
|
258
|
+
if (p.execs.some((e) => e.kind === 'CICS' && /^(VERIFY|SIGNON)$/.test(execReading(e).verb || ''))) return [];
|
|
279
259
|
const byName = new Map();
|
|
280
260
|
for (const it of p.items) if (!byName.has(it.name)) byName.set(it.name, it);
|
|
281
261
|
const resolve = (tok) => (tok && tok.t === 'word' ? p.resolved.get(tok) || byName.get(tok.u) || null : null);
|
|
@@ -286,8 +266,8 @@ export function* scanCicsSteps(root, opts = {}) {
|
|
|
286
266
|
// FETCH fills, and everything under any of them. A copy made by MOVE first is not followed.
|
|
287
267
|
const read = new Set();
|
|
288
268
|
for (const e of p.execs) {
|
|
289
|
-
const { opts: o,
|
|
290
|
-
if (e.kind === 'CICS' && /^READ(NEXT|PREV)?$/.test(
|
|
269
|
+
const { opts: o, verb } = execReading(e);
|
|
270
|
+
if (e.kind === 'CICS' && /^READ(NEXT|PREV)?$/.test(verb || '')) for (const t of o.get('INTO') || []) if (t.t === 'word') read.add(t.u);
|
|
291
271
|
if (e.kind === 'SQL' && (e.hostVariables || []).length) for (const h of e.hostVariables) if (h.written && h.tok) read.add(h.tok.u);
|
|
292
272
|
}
|
|
293
273
|
const stored = (name) => {
|
|
@@ -343,15 +323,15 @@ export function* scanCicsSteps(root, opts = {}) {
|
|
|
343
323
|
const sends = [];
|
|
344
324
|
for (const e of p.execs) {
|
|
345
325
|
if (e.kind !== 'CICS') continue;
|
|
346
|
-
const { opts: o,
|
|
347
|
-
const opt =
|
|
326
|
+
const { opts: o, verb } = execReading(e);
|
|
327
|
+
const opt = verb === 'XCTL' || verb === 'LINK' ? 'PROGRAM' : (verb === 'RETURN' || verb === 'START') ? 'TRANSID' : null;
|
|
348
328
|
const t = opt && (o.get(opt) || [])[0];
|
|
349
329
|
if (!t || t.t !== 'lit') continue;
|
|
350
330
|
const target = `${opt === 'PROGRAM' ? '' : 'transaction '}${String(t.v).trim().toUpperCase()}`;
|
|
351
331
|
if (target === p.id) continue;
|
|
352
332
|
const bits = ctl.facts.get(ctl.nodeOf.get(e));
|
|
353
333
|
if (!bits) continue;
|
|
354
|
-
sends.push({ e, verb
|
|
334
|
+
sends.push({ e, verb, target, signedOn: hasFact(bits, bit) });
|
|
355
335
|
}
|
|
356
336
|
const granted = new Set(sends.filter((x) => x.signedOn).map((x) => x.target));
|
|
357
337
|
const aid = p.refs.some((r) => r.tok.u === 'EIBAID');
|
package/lib/sets/compile.mjs
CHANGED
|
@@ -59,11 +59,13 @@ const REPLACE_STATEMENT = /\bREPLACE\b([\s\S]*?)(?:\.\s|\.$)/gim;
|
|
|
59
59
|
|
|
60
60
|
// A header declares a paragraph only where the sentence before it has ended: after a line of code
|
|
61
61
|
// ending in a period, or with no code before it. Without the period the name is read as part of the
|
|
62
|
-
// statement before, and the compiler reports it undefined.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
// statement before, and the compiler reports it undefined. `lines` and `starts` are the text's lines
|
|
63
|
+
// and where each begins.
|
|
64
|
+
function sentenceEndsBefore(lines, starts, at) {
|
|
65
|
+
let lo = 0;
|
|
66
|
+
let hi = starts.length - 1;
|
|
67
|
+
while (lo < hi) { const mid = (lo + hi + 1) >>> 1; if (starts[mid] <= at) lo = mid; else hi = mid - 1; }
|
|
68
|
+
for (let i = lo - 1; i >= 0; i--) {
|
|
67
69
|
const l = lines[i];
|
|
68
70
|
if (/^.{6}[*/]/.test(l) || /^\s*\*>/.test(l)) continue;
|
|
69
71
|
const code = l.slice(0, 72).replace(/\*>.*$/, '').replace(/^[0-9 ]{6}(?=[ \-D])/, '').trim();
|
|
@@ -72,12 +74,17 @@ function sentenceEndsBefore(text, at) {
|
|
|
72
74
|
return true;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
// Every name a text declares, upper-cased, whatever is being looked for.
|
|
78
|
+
function declaredWords(text) {
|
|
79
|
+
const words = new Set();
|
|
80
|
+
const note = (w) => words.add(w.toUpperCase());
|
|
81
|
+
const lines = text.split(/\r?\n/);
|
|
82
|
+
const starts = [0];
|
|
83
|
+
for (const m of text.matchAll(/\r?\n/g)) starts.push(m.index + m[0].length);
|
|
77
84
|
for (const re of [DECLARATION, HEADER]) {
|
|
78
85
|
re.lastIndex = 0;
|
|
79
86
|
let m;
|
|
80
|
-
while ((m = re.exec(text))) if (re !== HEADER || sentenceEndsBefore(
|
|
87
|
+
while ((m = re.exec(text))) if (re !== HEADER || sentenceEndsBefore(lines, starts, m.index)) note(m[1]);
|
|
81
88
|
}
|
|
82
89
|
for (const re of [COPY_STATEMENT, REPLACE_STATEMENT]) {
|
|
83
90
|
re.lastIndex = 0;
|
|
@@ -87,9 +94,11 @@ function declaredIn(text, names, into) {
|
|
|
87
94
|
for (const w of m[1].match(/[A-Za-z0-9][A-Za-z0-9-]*/g) || []) note(w);
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
|
-
return
|
|
97
|
+
return words;
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
const declaredIn = (words, names, into) => { for (const u of names) if (words.has(u)) into.add(u); return into; };
|
|
101
|
+
|
|
93
102
|
// A reference cut at column 72 names a word the source does not hold.
|
|
94
103
|
function truncated(ref, lines) {
|
|
95
104
|
const line = (lines.get(ref.file) || [])[ref.line - 1] || '';
|
|
@@ -114,7 +123,23 @@ export function* scanCompileSteps(root, opts = {}) {
|
|
|
114
123
|
because[why] = (because[why] || 0) + 1;
|
|
115
124
|
undecided.push(`${tree.rel(f)} (${id}): ${why}: ${[...new Set(refs.map((x) => x.name))].slice(0, SHOWN).join(', ')}`);
|
|
116
125
|
};
|
|
117
|
-
|
|
126
|
+
// A copybook's text, lines and declared names, read once for every program that copies it.
|
|
127
|
+
const remembered = (read) => {
|
|
128
|
+
const held = new Map();
|
|
129
|
+
return (p) => {
|
|
130
|
+
let h = held.get(p);
|
|
131
|
+
if (!h) {
|
|
132
|
+
try { const text = read(p); h = { text, lines: null, words: null }; } catch (e) { h = { error: e }; }
|
|
133
|
+
held.set(p, h);
|
|
134
|
+
}
|
|
135
|
+
if (h.error) throw h.error;
|
|
136
|
+
return h;
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
const copyOf = remembered((p) => (tree.contains(p) ? tree.text(p) : readSource(p)).text);
|
|
140
|
+
const answerOf = remembered((p) => tree.text(p).text);
|
|
141
|
+
const wordsOf = (h) => (h.words ||= declaredWords(h.text));
|
|
142
|
+
const linesOf = (h) => (h.lines ||= h.text.split(/\r?\n/));
|
|
118
143
|
const candidates = [];
|
|
119
144
|
|
|
120
145
|
const run = yield loopOver(files, (f) => {
|
|
@@ -126,6 +151,7 @@ export function* scanCompileSteps(root, opts = {}) {
|
|
|
126
151
|
stats.filesScanned++;
|
|
127
152
|
const blocked = whyUndecided(r, src);
|
|
128
153
|
const copied = [...new Set(r.copies.filter((c) => c.path).map((c) => c.path))];
|
|
154
|
+
const own = { text: src, lines: null, words: null };
|
|
129
155
|
for (const p of r.programs) {
|
|
130
156
|
// A class or a method has no PROGRAM-ID, and the data its methods use belongs to no program here.
|
|
131
157
|
if (!p.id || p.diags.some((d) => d.kind === 'no-procedure-division')) continue;
|
|
@@ -133,11 +159,11 @@ export function* scanCompileSteps(root, opts = {}) {
|
|
|
133
159
|
if (!p.unresolvedRefs.length) continue;
|
|
134
160
|
if (blocked) { undecide(f, p.id, blocked, p.unresolvedRefs); continue; }
|
|
135
161
|
const names = new Set(p.unresolvedRefs.map((x) => x.name));
|
|
136
|
-
const texts = new Map([[f,
|
|
137
|
-
for (const q of copied) { try { texts.set(q,
|
|
162
|
+
const texts = new Map([[f, own]]);
|
|
163
|
+
for (const q of copied) { try { texts.set(q, copyOf(q)); } catch { /* the parse read it once; a second failure changes nothing */ } }
|
|
138
164
|
const seen = new Set();
|
|
139
|
-
for (const
|
|
140
|
-
const lines = new Map([...texts].map(([q,
|
|
165
|
+
for (const h of texts.values()) declaredIn(wordsOf(h), names, seen);
|
|
166
|
+
const lines = new Map([...texts].map(([q, h]) => [q, linesOf(h)]));
|
|
141
167
|
const left = p.unresolvedRefs.filter((x) => !seen.has(x.name) && !truncated(x, lines));
|
|
142
168
|
if (!left.length) { undecide(f, p.id, 'the parse missed a declaration the text holds', p.unresolvedRefs); continue; }
|
|
143
169
|
candidates.push({ file: f, id: p.id, line: p.line, refs: left, copied: new Set(r.copies.map((c) => nameOf(c.name))) });
|
|
@@ -160,7 +186,7 @@ export function* scanCompileSteps(root, opts = {}) {
|
|
|
160
186
|
for (const c of candidates) {
|
|
161
187
|
const names = new Set(c.refs.map((x) => x.name));
|
|
162
188
|
const seen = new Set();
|
|
163
|
-
for (const n of c.copied) for (const p of answering.get(n) || []) { try { declaredIn(
|
|
189
|
+
for (const n of c.copied) for (const p of answering.get(n) || []) { try { declaredIn(wordsOf(answerOf(p)), names, seen); } catch { /* unread declares nothing */ } }
|
|
164
190
|
const left = c.refs.filter((x) => !seen.has(x.name));
|
|
165
191
|
if (!left.length) { undecide(c.file, c.id, 'another copybook of a name it copies declares them', c.refs); continue; }
|
|
166
192
|
const firsts = new Map();
|
package/lib/sets/crypto.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { readFileSync } from 'node:fs';
|
|
|
11
11
|
import { inScope, isProgram, relPath } from '../sources.mjs';
|
|
12
12
|
import { report } from '../kernel/ruleset.mjs';
|
|
13
13
|
import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { drive, loopOver } from '../kernel/shared-pass.mjs';
|
|
15
15
|
import { constantsReaching, keywordsOf } from '../constants.mjs';
|
|
16
16
|
|
|
17
17
|
const TABLE = JSON.parse(readFileSync(new URL('../../rules/icsf-services.json', import.meta.url), 'utf8'));
|
|
@@ -54,13 +54,13 @@ const argument = (call, service, parameter) => {
|
|
|
54
54
|
};
|
|
55
55
|
const shown = (values) => [...new Set(values.map((v) => (/^[A-Z-]+$/.test(v) ? v : `'${v.trim()}'`)))].join(', ');
|
|
56
56
|
|
|
57
|
-
export function
|
|
57
|
+
export function* scanCryptoSteps(root, opts = {}) {
|
|
58
58
|
const tree = treeFor(root, opts);
|
|
59
59
|
const files = tree.list().filter(isProgram).filter(inScope(opts));
|
|
60
60
|
const findings = [];
|
|
61
61
|
const stats = { filesScanned: 0, filesUnreadable: 0, filesUnparsed: 0, icsfCalls: 0, argumentsUndecided: 0 };
|
|
62
62
|
|
|
63
|
-
const run =
|
|
63
|
+
const run = yield loopOver(files, (f) => {
|
|
64
64
|
let src;
|
|
65
65
|
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
66
66
|
if (!/CALL\s+['"]?CS[NF]/i.test(src)) return src.length;
|
|
@@ -108,3 +108,5 @@ export function scanCrypto(root, opts = {}) {
|
|
|
108
108
|
|
|
109
109
|
return report('crypto', { rules: CRYPTO_RULES, findings, stats, run });
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
export const scanCrypto = (root, opts = {}) => drive(scanCryptoSteps(root, opts));
|