@portll/cobolwork 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -26
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +56 -14
- package/lib/baseline.mjs +9 -0
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +133 -53
- package/lib/capabilities.mjs +13 -11
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +17 -0
- package/lib/control-reuse.mjs +113 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +77 -4
- package/lib/dataflow.mjs +402 -152
- package/lib/db2/cursor.mjs +15 -0
- package/lib/db2/read.mjs +170 -0
- package/lib/db2/rules.mjs +77 -0
- package/lib/db2/stmt/alter.mjs +473 -0
- package/lib/db2/stmt/grant.mjs +125 -0
- package/lib/db2/stmt/index.mjs +141 -0
- package/lib/db2/stmt/misc.mjs +325 -0
- package/lib/db2/stmt/routine.mjs +564 -0
- package/lib/db2/stmt/storage.mjs +146 -0
- package/lib/db2/stmt/table.mjs +540 -0
- package/lib/db2/stmt/view.mjs +146 -0
- package/lib/diff.mjs +17 -5
- package/lib/equivalence.mjs +44 -5
- package/lib/evidence/cli.mjs +26 -7
- package/lib/evidence/record.mjs +16 -6
- package/lib/evidence/seal.mjs +17 -0
- package/lib/evidence/store.mjs +44 -15
- package/lib/evidence/timestamp.mjs +83 -0
- package/lib/evidence/verify.mjs +120 -42
- package/lib/exec-reading.mjs +51 -0
- package/lib/execution.mjs +3 -2
- package/lib/explain.mjs +2 -0
- package/lib/exploitability.mjs +11 -2
- package/lib/hlasm/asm/data.mjs +128 -0
- package/lib/hlasm/asm/listing.mjs +43 -0
- package/lib/hlasm/asm/output.mjs +45 -0
- package/lib/hlasm/asm/sections.mjs +161 -0
- package/lib/hlasm/asm/symbols.mjs +78 -0
- package/lib/hlasm/exec.mjs +27 -0
- package/lib/hlasm/expr.mjs +167 -0
- package/lib/hlasm/instr.mjs +73 -0
- package/lib/hlasm/locate.mjs +333 -0
- package/lib/hlasm/macro/authorization.mjs +146 -0
- package/lib/hlasm/macro/datasets.mjs +107 -0
- package/lib/hlasm/macro/io.mjs +162 -0
- package/lib/hlasm/macro/le.mjs +53 -0
- package/lib/hlasm/macro/linkage.mjs +143 -0
- package/lib/hlasm/macro/operator.mjs +77 -0
- package/lib/hlasm/macro/program.mjs +184 -0
- package/lib/hlasm/macro/recovery.mjs +85 -0
- package/lib/hlasm/macro/storage.mjs +167 -0
- package/lib/hlasm/macro/structured.mjs +131 -0
- package/lib/hlasm/model.mjs +97 -0
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +59 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +130 -0
- package/lib/hlasm.mjs +44 -7
- package/lib/ims/dli.mjs +37 -0
- package/lib/ims/macro/dbd.mjs +299 -0
- package/lib/ims/macro/psb.mjs +286 -0
- package/lib/ims/model.mjs +149 -0
- package/lib/ims/operands.mjs +23 -0
- package/lib/ims/read.mjs +37 -0
- package/lib/ims/rules.mjs +135 -0
- package/lib/inventory.mjs +10 -7
- package/lib/ironwork-ids.mjs +24 -0
- package/lib/ironwork.mjs +20 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +32 -23
- package/lib/kernel/shared-pass.mjs +163 -0
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- package/lib/options.mjs +26 -4
- package/lib/parser.mjs +136 -17
- package/lib/pli/cursor.mjs +15 -0
- package/lib/pli/expr.mjs +101 -0
- package/lib/pli/include.mjs +82 -0
- package/lib/pli/layout.mjs +125 -0
- package/lib/pli/lex.mjs +198 -0
- package/lib/pli/program.mjs +280 -0
- package/lib/pli/rules/based.mjs +95 -0
- package/lib/pli/rules/conditions.mjs +68 -0
- package/lib/pli/rules/entry.mjs +130 -0
- package/lib/pli/rules/index.mjs +24 -0
- package/lib/pli/rules/preprocessor.mjs +55 -0
- package/lib/pli/statements.mjs +130 -0
- package/lib/pli/stmt/alloc.mjs +45 -0
- package/lib/pli/stmt/assignment.mjs +56 -0
- package/lib/pli/stmt/call.mjs +104 -0
- package/lib/pli/stmt/conditions.mjs +94 -0
- package/lib/pli/stmt/control.mjs +219 -0
- package/lib/pli/stmt/declare.mjs +149 -0
- package/lib/pli/stmt/exec.mjs +55 -0
- package/lib/pli/stmt/io.mjs +239 -0
- package/lib/pli/stmt/misc.mjs +4 -0
- package/lib/pli/stmt/preprocessor.mjs +242 -0
- package/lib/pli/stmt/procedure.mjs +258 -0
- package/lib/pli/stmt/stream.mjs +283 -0
- package/lib/pli/storage.mjs +129 -0
- package/lib/policy.mjs +6 -0
- package/lib/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +8 -4
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +44 -3
- package/lib/scan.mjs +28 -12
- package/lib/sets/abend.mjs +82 -8
- package/lib/sets/build.mjs +43 -37
- package/lib/sets/cics.mjs +20 -38
- package/lib/sets/compile.mjs +46 -18
- package/lib/sets/copybook.mjs +5 -3
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +32 -8
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +124 -8
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +11 -9
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/priv.mjs +5 -3
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +112 -0
- package/lib/sets/semantics.mjs +8 -3
- package/lib/sets/web.mjs +39 -23
- package/lib/site.mjs +10 -0
- package/lib/sources.mjs +80 -20
- package/lib/statement-cursor.mjs +67 -0
- package/lib/verify.mjs +3 -2
- package/lib/version.mjs +6 -0
- package/package.json +3 -2
- package/rules/compliance-cobit2019.json +520 -5
- package/rules/compliance-dora.json +509 -5
- package/rules/compliance-ffiec.json +505 -1
- package/rules/compliance-nist80053.json +557 -1
- package/rules/gitleaks-mainframe.toml +46 -15
- package/rules/hlasm-instructions.json +2396 -0
- package/rules/hlasm-optables.json +8024 -0
- package/schema/cobolwork-baseline.schema.json +36 -0
- package/schema/cobolwork-build-provenance.schema.json +187 -0
- package/schema/cobolwork-build.schema.json +382 -0
- package/schema/cobolwork-capabilities.schema.json +239 -0
- package/schema/cobolwork-diff.schema.json +217 -0
- package/schema/cobolwork-evidence.schema.json +161 -0
- package/schema/cobolwork-execution.schema.json +53 -0
- package/schema/cobolwork-explain.schema.json +360 -0
- package/schema/cobolwork-finding.schema.json +465 -0
- package/schema/cobolwork-flow.schema.json +465 -0
- package/schema/cobolwork-gate.schema.json +211 -0
- package/schema/cobolwork-inventory.schema.json +206 -0
- package/schema/cobolwork-parse.schema.json +105 -0
- package/schema/cobolwork-reach.schema.json +74 -0
- package/schema/cobolwork-report.schema.json +559 -0
- package/schema/cobolwork-witness.schema.json +107 -0
- package/schema/cobolwork.baseline.schema.json +101 -0
- package/schema/cobolwork.policy.schema.json +7 -0
- package/schema/cobolwork.site.schema.json +116 -0
package/lib/sets/compile.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { basename, extname } from 'node:path';
|
|
|
13
13
|
import { inScope, isProgram, relPath, readSource } from '../sources.mjs';
|
|
14
14
|
import { report } from '../kernel/ruleset.mjs';
|
|
15
15
|
import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
|
|
16
|
-
import {
|
|
16
|
+
import { drive, loopOver } from '../kernel/shared-pass.mjs';
|
|
17
17
|
|
|
18
18
|
// construct: no input has to reach it. med: certain once every copybook is found, and it means the
|
|
19
19
|
// tree does not hold what runs, but it is not exploitable in itself. CWE-1127: MITRE discourages
|
|
@@ -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] || '';
|
|
@@ -102,7 +111,7 @@ function truncated(ref, lines) {
|
|
|
102
111
|
return any && !whole;
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
export function
|
|
114
|
+
export function* scanCompileSteps(root, opts = {}) {
|
|
106
115
|
const tree = treeFor(root, opts);
|
|
107
116
|
const files = tree.list().filter(isProgram).filter(inScope(opts));
|
|
108
117
|
const findings = [];
|
|
@@ -114,10 +123,26 @@ export function scanCompile(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
|
-
const run =
|
|
145
|
+
const run = yield loopOver(files, (f) => {
|
|
121
146
|
let src;
|
|
122
147
|
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
123
148
|
if (LISTING.test(src)) { stats.filesScanned++; stats.listingsSkipped = (stats.listingsSkipped || 0) + 1; return src.length; }
|
|
@@ -126,6 +151,7 @@ export function scanCompile(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 scanCompile(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 scanCompile(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();
|
|
@@ -186,3 +212,5 @@ export function scanCompile(root, opts = {}) {
|
|
|
186
212
|
if (stats.filesUnparsed) stats.coverageIncomplete = true;
|
|
187
213
|
return report('compile', { rules: COMPILE_RULES, findings, stats, run });
|
|
188
214
|
}
|
|
215
|
+
|
|
216
|
+
export const scanCompile = (root, opts = {}) => drive(scanCompileSteps(root, opts));
|
package/lib/sets/copybook.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { inScope, isCopybook, isProgram, relPath } from '../sources.mjs';
|
|
|
6
6
|
import { finish } from '../kernel/findings.mjs';
|
|
7
7
|
import { report } from '../kernel/ruleset.mjs';
|
|
8
8
|
import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { drive, loopOver } from '../kernel/shared-pass.mjs';
|
|
10
10
|
import { cobolCard } from '../cards.mjs';
|
|
11
11
|
|
|
12
12
|
// A copybook is resolved by name along a search path: the program's own directory first, then the
|
|
@@ -71,7 +71,7 @@ export function systemLayoutDeparture(name, text) {
|
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export function
|
|
74
|
+
export function* scanCopybooksSteps(root, opts = {}) {
|
|
75
75
|
const tree = treeFor(root, opts);
|
|
76
76
|
const all = tree.list().filter(inScope(opts));
|
|
77
77
|
const findings = [];
|
|
@@ -100,7 +100,7 @@ export function scanCopybooks(root, opts = {}) {
|
|
|
100
100
|
// is what stops the reading before the heap does.
|
|
101
101
|
let run = { skipped: [], stoppedBy: null, peakHeapBytes: 0, note: null };
|
|
102
102
|
if (candidates.size) {
|
|
103
|
-
run =
|
|
103
|
+
run = yield loopOver(all.filter(isProgram), (p) => {
|
|
104
104
|
let src;
|
|
105
105
|
try { src = tree.text(p).text; } catch (e) { noteUnread(stats, tree, p, e); return 0; }
|
|
106
106
|
let r;
|
|
@@ -173,3 +173,5 @@ export function scanCopybooks(root, opts = {}) {
|
|
|
173
173
|
// that looks unshadowed may only look that way.
|
|
174
174
|
return report('copybook', { rules: COPYBOOK_RULES, findings, stats, run });
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
export const scanCopybooks = (root, opts = {}) => drive(scanCopybooksSteps(root, opts));
|
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));
|
package/lib/sets/ddl.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Db2 for z/OS DDL: what a GRANT opens to PUBLIC or lets a grantee pass on, system and database
|
|
3
|
+
// authorities granted, and the exit routines and external load modules that run inside Db2
|
|
4
|
+
// (lib/db2/rules.mjs). A statement in another dialect is refused by the reader and counted as not
|
|
5
|
+
// read, never matched as if it were Db2.
|
|
6
|
+
import { inScope, isDb2, relPath } from '../sources.mjs';
|
|
7
|
+
import { report } from '../kernel/ruleset.mjs';
|
|
8
|
+
import { treeFor, noteUnread } from '../kernel/source-tree.mjs';
|
|
9
|
+
import { eachWithinMemory } from '../kernel/memory.mjs';
|
|
10
|
+
import { readDb2, parseDb2Statement } from '../db2/read.mjs';
|
|
11
|
+
import { DB2_RULES, statementFindings } from '../db2/rules.mjs';
|
|
12
|
+
|
|
13
|
+
export { DB2_RULES };
|
|
14
|
+
|
|
15
|
+
export function scanDb2(root, opts = {}) {
|
|
16
|
+
const tree = treeFor(root, opts);
|
|
17
|
+
const files = tree.list().filter(inScope(opts)).filter(isDb2);
|
|
18
|
+
const stats = { filesScanned: 0, filesUnreadable: 0, statementsRead: 0 };
|
|
19
|
+
const findings = [];
|
|
20
|
+
const unread = {};
|
|
21
|
+
const run = eachWithinMemory(files, (f) => {
|
|
22
|
+
let src;
|
|
23
|
+
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
24
|
+
stats.filesScanned++;
|
|
25
|
+
const path = relPath(root, f);
|
|
26
|
+
for (const st of readDb2(src).statements) {
|
|
27
|
+
const r = parseDb2Statement(st);
|
|
28
|
+
if (r.status === 'parsed') { stats.statementsRead++; findings.push(...statementFindings(path, st, r)); }
|
|
29
|
+
else (unread[r.kind] ||= { count: 0, first: `${path}:${st.line}` }).count++;
|
|
30
|
+
}
|
|
31
|
+
return src.length;
|
|
32
|
+
}, { label: 'ddl', maxBytes: opts.maxSourceBytes ?? Infinity });
|
|
33
|
+
|
|
34
|
+
if (Object.keys(unread).length) { stats.statementsNotRead = unread; stats.coverageIncomplete = true; }
|
|
35
|
+
return report('ddl', { rules: DB2_RULES, findings, stats, run });
|
|
36
|
+
}
|
package/lib/sets/flow.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
import {
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { directoryTree } from '../kernel/source-tree.mjs';
|
|
4
|
+
import { analyzeSteps } from '../dataflow.mjs';
|
|
5
|
+
import { drive } from '../kernel/shared-pass.mjs';
|
|
3
6
|
import { FLOW_MODEL, SCHEMA_VERSION, TOOL_VERSION } from '../version.mjs';
|
|
4
7
|
import { finish, withMeta, sortFindings } from '../kernel/findings.mjs';
|
|
5
8
|
|
|
@@ -11,21 +14,25 @@ export const RULES = {
|
|
|
11
14
|
'cics-terminal-to-os-command': { sev: 'crit', evidence: 'path', cwe: 'CWE-78', text: 'Terminal input reaches an operating-system command routine' },
|
|
12
15
|
'cics-web-to-os-command': { sev: 'crit', evidence: 'path', cwe: 'CWE-78', text: 'Web input reaches an operating-system command routine' },
|
|
13
16
|
'file-record-to-os-command': { sev: 'high', evidence: 'path', cwe: 'CWE-78', text: 'A file record reaches an operating-system command routine' },
|
|
17
|
+
'cics-queue-to-os-command': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-78', text: 'A CICS queue item reaches an operating-system command routine' },
|
|
14
18
|
'database-to-os-command': { sev: 'high', evidence: 'path', cwe: 'CWE-78', text: 'A database value reaches an operating-system command routine' },
|
|
15
19
|
'argv-or-env-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'Command-line or environment input reaches dynamic SQL' },
|
|
16
20
|
'cics-terminal-to-dynamic-sql': { sev: 'crit', evidence: 'path', cwe: 'CWE-89', text: 'Terminal input reaches dynamic SQL' },
|
|
17
21
|
'cics-web-to-dynamic-sql': { sev: 'crit', evidence: 'path', cwe: 'CWE-89', text: 'Web input reaches dynamic SQL' },
|
|
18
22
|
'file-record-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'A file record reaches dynamic SQL' },
|
|
23
|
+
'cics-queue-to-dynamic-sql': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-89', text: 'A CICS queue item reaches dynamic SQL' },
|
|
19
24
|
'database-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'A database value reaches dynamic SQL' },
|
|
20
25
|
'argv-or-env-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'Command-line or environment input decides which program CICS transfers to' },
|
|
21
26
|
'cics-terminal-to-cics-dynamic-transfer': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Terminal input decides which program CICS transfers to' },
|
|
22
27
|
'cics-web-to-cics-dynamic-transfer': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Web input decides which program CICS transfers to' },
|
|
23
28
|
'file-record-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A file record decides which program CICS transfers to' },
|
|
29
|
+
'cics-queue-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-470', text: 'A CICS queue item decides which program CICS transfers to' },
|
|
24
30
|
'database-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A database value decides which program CICS transfers to' },
|
|
25
31
|
'argv-or-env-to-dynamic-program-load': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'Command-line or environment input decides which program is called' },
|
|
26
32
|
'cics-terminal-to-dynamic-program-load': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Terminal input decides which program is called' },
|
|
27
33
|
'cics-web-to-dynamic-program-load': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Web input decides which program is called' },
|
|
28
34
|
'file-record-to-dynamic-program-load': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A file record decides which program is called' },
|
|
35
|
+
'cics-queue-to-dynamic-program-load': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-470', text: 'A CICS queue item decides which program is called' },
|
|
29
36
|
'database-to-dynamic-program-load': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A database value decides which program is called' },
|
|
30
37
|
// A program opening the file its own user named acts with that user's authority, which is every
|
|
31
38
|
// one read in the 500-repository corpus; it escalates only where the estate says the entry that
|
|
@@ -34,6 +41,7 @@ export const RULES = {
|
|
|
34
41
|
'cics-terminal-to-dynamic-file-path': { sev: 'high', evidence: 'path', cwe: 'CWE-73', text: 'Terminal input decides which file is opened' },
|
|
35
42
|
'cics-web-to-dynamic-file-path': { sev: 'high', evidence: 'path', cwe: 'CWE-73', text: 'Web input decides which file is opened' },
|
|
36
43
|
'file-record-to-dynamic-file-path': { sev: 'med', evidence: 'path', cwe: 'CWE-73', text: 'A file record decides which file is opened' },
|
|
44
|
+
'cics-queue-to-dynamic-file-path': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-73', text: 'A CICS queue item decides which file is opened' },
|
|
37
45
|
'database-to-dynamic-file-path': { sev: 'med', evidence: 'path', cwe: 'CWE-73', text: 'A database value decides which file is opened' },
|
|
38
46
|
// A JCL step is the entry point above the program. PARM is the mainframe's argv, and in-stream
|
|
39
47
|
// data is a payload sitting in the repository: both are edited by anyone who can commit, so they
|
|
@@ -57,6 +65,7 @@ export const RULES = {
|
|
|
57
65
|
'cics-terminal-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'Terminal input is written into a job the internal reader submits' },
|
|
58
66
|
'cics-web-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'Web input is written into a job the internal reader submits' },
|
|
59
67
|
'file-record-to-internal-reader': { sev: 'med', evidence: 'path', cwe: 'CWE-77', text: 'A file record is written into a job the internal reader submits' },
|
|
68
|
+
'cics-queue-to-internal-reader': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-77', text: 'A CICS queue item is written into a job the internal reader submits' },
|
|
60
69
|
'database-to-internal-reader': { sev: 'med', evidence: 'path', cwe: 'CWE-77', text: 'A database value is written into a job the internal reader submits' },
|
|
61
70
|
'jcl-parm-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'A JCL PARM is written into a job the internal reader submits' },
|
|
62
71
|
'jcl-instream-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'In-stream job data is written into a job the internal reader submits' },
|
|
@@ -65,18 +74,22 @@ export const RULES = {
|
|
|
65
74
|
'cics-web-to-web-response': { sev: 'high', evidence: 'path', cwe: 'CWE-79', text: 'Web input is written into the response this program returns' },
|
|
66
75
|
'cics-terminal-to-web-response': { sev: 'high', evidence: 'path', cwe: 'CWE-79', text: 'Terminal input is written into the response this program returns' },
|
|
67
76
|
'file-record-to-web-response': { sev: 'med', evidence: 'path', cwe: 'CWE-79', text: 'A file record is written into the response this program returns' },
|
|
77
|
+
'cics-queue-to-web-response': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-79', text: 'A CICS queue item is written into the response this program returns' },
|
|
68
78
|
'database-to-web-response': { sev: 'med', evidence: 'path', cwe: 'CWE-79', text: 'A database value is written into the response this program returns' },
|
|
69
79
|
'cics-web-to-http-header': { sev: 'high', evidence: 'path', cwe: 'CWE-113', text: 'Web input is written into a header of the response' },
|
|
70
80
|
'cics-terminal-to-http-header': { sev: 'high', evidence: 'path', cwe: 'CWE-113', text: 'Terminal input is written into a header of the response' },
|
|
71
81
|
'file-record-to-http-header': { sev: 'med', evidence: 'path', cwe: 'CWE-113', text: 'A file record is written into a header of the response' },
|
|
82
|
+
'cics-queue-to-http-header': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-113', text: 'A CICS queue item is written into a header of the response' },
|
|
72
83
|
'database-to-http-header': { sev: 'med', evidence: 'path', cwe: 'CWE-113', text: 'A database value is written into a header of the response' },
|
|
73
84
|
'cics-web-to-outbound-host': { sev: 'high', evidence: 'path', cwe: 'CWE-918', text: 'Web input decides the host or path this program sends a request to' },
|
|
74
85
|
'cics-terminal-to-outbound-host': { sev: 'high', evidence: 'path', cwe: 'CWE-918', text: 'Terminal input decides the host or path this program sends a request to' },
|
|
75
86
|
'file-record-to-outbound-host': { sev: 'med', evidence: 'path', cwe: 'CWE-918', text: 'A file record decides the host or path this program sends a request to' },
|
|
87
|
+
'cics-queue-to-outbound-host': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-918', text: 'A CICS queue item decides the host or path this program sends a request to' },
|
|
76
88
|
'database-to-outbound-host': { sev: 'med', evidence: 'path', cwe: 'CWE-918', text: 'A database value decides the host or path this program sends a request to' },
|
|
77
89
|
'cics-web-to-queue-name': { sev: 'high', evidence: 'path', cwe: 'CWE-99', text: 'Web input names the queue a command reads, writes or deletes' },
|
|
78
90
|
'cics-terminal-to-queue-name': { sev: 'high', evidence: 'path', cwe: 'CWE-99', text: 'Terminal input names the queue a command reads, writes or deletes' },
|
|
79
91
|
'file-record-to-queue-name': { sev: 'med', evidence: 'path', cwe: 'CWE-99', text: 'A file record names the queue a command reads, writes or deletes' },
|
|
92
|
+
'cics-queue-to-queue-name': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-99', text: 'A CICS queue item names the queue a command reads, writes or deletes' },
|
|
80
93
|
'database-to-queue-name': { sev: 'med', evidence: 'path', cwe: 'CWE-99', text: 'A database value names the queue a command reads, writes or deletes' },
|
|
81
94
|
// Where a statement reads or writes, decided by input from outside. Without SSRANGE, which is not
|
|
82
95
|
// the Enterprise COBOL default, an index out of range reads or writes the storage beside the
|
|
@@ -130,10 +143,12 @@ export const RULES = {
|
|
|
130
143
|
'cics-terminal-to-cics-sysid': { sev: 'med', evidence: 'path', cwe: 'CWE-15', text: 'Terminal input decides which region a CICS command is shipped to' },
|
|
131
144
|
'cics-web-to-cics-sysid': { sev: 'med', evidence: 'path', cwe: 'CWE-15', text: 'Web input decides which region a CICS command is shipped to' },
|
|
132
145
|
'file-record-to-cics-sysid': { sev: 'low', evidence: 'path', cwe: 'CWE-15', text: 'A file record decides which region a CICS command is shipped to' },
|
|
146
|
+
'cics-queue-to-cics-sysid': { sev: 'low', evidence: 'path', measured: false, cwe: 'CWE-15', text: 'A CICS queue item decides which region a CICS command is shipped to' },
|
|
133
147
|
'database-to-cics-sysid': { sev: 'low', evidence: 'path', cwe: 'CWE-15', text: 'A database value decides which region a CICS command is shipped to' },
|
|
134
148
|
'cics-terminal-to-cics-system-resource': { sev: 'high', evidence: 'path', cwe: 'CWE-15', text: 'Terminal input names the resource an EXEC CICS SET changes' },
|
|
135
149
|
'cics-web-to-cics-system-resource': { sev: 'high', evidence: 'path', cwe: 'CWE-15', text: 'Web input names the resource an EXEC CICS SET changes' },
|
|
136
150
|
'file-record-to-cics-system-resource': { sev: 'med', evidence: 'path', cwe: 'CWE-15', text: 'A file record names the resource an EXEC CICS SET changes' },
|
|
151
|
+
'cics-queue-to-cics-system-resource': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-15', text: 'A CICS queue item names the resource an EXEC CICS SET changes' },
|
|
137
152
|
'database-to-cics-system-resource': { sev: 'med', evidence: 'path', cwe: 'CWE-15', text: 'A database value names the resource an EXEC CICS SET changes' },
|
|
138
153
|
// A document from outside parsed as XML. Whether a DTD in it is honoured - an external entity
|
|
139
154
|
// fetched, an internal one expanded - is the parser's, chosen by the XMLPARSE compiler option, and
|
|
@@ -165,12 +180,16 @@ export const RULES = {
|
|
|
165
180
|
// way mainframe systems hand data to each other and is listed so the route is visible.
|
|
166
181
|
'database-to-outbound-http': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A database value is sent to a remote server over HTTP' },
|
|
167
182
|
'file-record-to-outbound-http': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A file record is sent to a remote server over HTTP' },
|
|
183
|
+
'cics-queue-to-outbound-http': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-201', text: 'A CICS queue item is sent to a remote server over HTTP' },
|
|
168
184
|
'database-to-socket-send': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A database value is written to a network socket' },
|
|
169
185
|
'file-record-to-socket-send': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A file record is written to a network socket' },
|
|
186
|
+
'cics-queue-to-socket-send': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-201', text: 'A CICS queue item is written to a network socket' },
|
|
170
187
|
'database-to-message-queue': { sev: 'low', evidence: 'path', cwe: 'CWE-201', text: 'A database value is put on a message queue' },
|
|
171
188
|
'database-to-extrapartition-queue': { sev: 'low', evidence: 'path', cwe: 'CWE-201', text: 'A database value is written to a transient-data queue that leaves the region' },
|
|
172
189
|
'file-record-to-extrapartition-queue': { sev: 'low', evidence: 'path', cwe: 'CWE-201', text: 'A file record is written to a transient-data queue that leaves the region' },
|
|
190
|
+
'cics-queue-to-extrapartition-queue': { sev: 'low', evidence: 'path', measured: false, cwe: 'CWE-201', text: 'A CICS queue item is written to a transient-data queue that leaves the region' },
|
|
173
191
|
'file-record-to-message-queue': { sev: 'low', evidence: 'path', cwe: 'CWE-201', text: 'A file record is put on a message queue' },
|
|
192
|
+
'cics-queue-to-message-queue': { sev: 'low', evidence: 'path', measured: false, cwe: 'CWE-201', text: 'A CICS queue item is put on a message queue' },
|
|
174
193
|
// The CALL interface itself, from the same bound pairs the paths above cross. No input has to
|
|
175
194
|
// reach these: a callee that declares more than it is passed reaches past it on every call.
|
|
176
195
|
'call-parameter-exceeds-caller-record': { sev: 'high', evidence: 'construct', cwe: 'CWE-805', text: 'A called program declares a parameter longer than the whole record its caller passes' },
|
|
@@ -190,6 +209,7 @@ export const WHO = {
|
|
|
190
209
|
'cics-terminal': 'A terminal user',
|
|
191
210
|
'cics-web': 'A web caller',
|
|
192
211
|
'file-record': 'Whoever can write the file record',
|
|
212
|
+
'cics-queue': 'Whoever can write the queue, or start the transaction with data',
|
|
193
213
|
'database': 'Whoever can write the database row',
|
|
194
214
|
'jcl-parm': "Whoever can edit the job's PARM",
|
|
195
215
|
'jcl-instream': "Whoever can edit the job's in-stream data",
|
|
@@ -216,7 +236,7 @@ const SINK = {
|
|
|
216
236
|
},
|
|
217
237
|
'dynamic-file-path': {
|
|
218
238
|
clause: 'chooses which dataset the program opens',
|
|
219
|
-
remedy: 'Map input to a fixed set of DDs or
|
|
239
|
+
remedy: 'Map input to a fixed set of DDs, dataset names or CICS file names; do not build the ASSIGN, dynamic-allocation or FILE name from input',
|
|
220
240
|
},
|
|
221
241
|
'internal-reader': {
|
|
222
242
|
clause: "submits an arbitrary job, for a CICS program under the region's authority",
|
|
@@ -365,11 +385,11 @@ function mergeRoutes(a, b) {
|
|
|
365
385
|
};
|
|
366
386
|
}
|
|
367
387
|
// One graph per repository, for the reason scanAll gives: names are only unique within one.
|
|
368
|
-
function analyzeEach(root, opts) {
|
|
369
|
-
if (!opts.repos || opts.repos.length < 2) return
|
|
388
|
+
function* analyzeEach(root, opts) {
|
|
389
|
+
if (!opts.repos || opts.repos.length < 2) return yield* analyzeSteps(root, opts);
|
|
370
390
|
const merged = { findings: [], constructs: [], unstarted: [], startedBy: {}, stats: {} };
|
|
371
391
|
for (const repo of opts.repos) {
|
|
372
|
-
const one =
|
|
392
|
+
const one = yield* analyzeSteps(root, { ...opts, repos: [repo] });
|
|
373
393
|
merged.findings.push(...one.findings);
|
|
374
394
|
merged.constructs.push(...one.constructs);
|
|
375
395
|
merged.unstarted.push(...one.unstarted);
|
|
@@ -386,8 +406,9 @@ function analyzeEach(root, opts) {
|
|
|
386
406
|
return merged;
|
|
387
407
|
}
|
|
388
408
|
|
|
389
|
-
export function
|
|
390
|
-
|
|
409
|
+
export function* scanSteps(root, opts = {}) {
|
|
410
|
+
root = resolve(root);
|
|
411
|
+
const res = yield* analyzeEach(root, opts);
|
|
391
412
|
const findings = [];
|
|
392
413
|
for (const f of res.findings) {
|
|
393
414
|
const meta = RULES[f.rule] || DEFAULT;
|
|
@@ -531,7 +552,8 @@ export function scan(root, opts = {}) {
|
|
|
531
552
|
// happened to compute the right answer for the whole report - it ORs filesOverBudget in
|
|
532
553
|
// separately - so nothing was wrong downstream, and nothing would have been until someone
|
|
533
554
|
// called this set on its own and believed a silence.
|
|
534
|
-
coverageIncomplete: (res.stats.
|
|
555
|
+
coverageIncomplete: (res.stats.files === 0 && directoryTree(root, opts).list().length === 0)
|
|
556
|
+
|| (res.stats.overBudget || 0) > 0
|
|
535
557
|
|| (res.stats.filesNotReached || 0) > 0
|
|
536
558
|
|| (res.stats.unreadable || 0) > 0
|
|
537
559
|
|| (res.stats.threw || 0) > 0
|
|
@@ -570,6 +592,8 @@ export function scan(root, opts = {}) {
|
|
|
570
592
|
return report;
|
|
571
593
|
}
|
|
572
594
|
|
|
595
|
+
export const scan = (root, opts = {}) => drive(scanSteps(root, opts));
|
|
596
|
+
|
|
573
597
|
// Findings counted by the transaction or job that reaches them: the unit a mainframe team triages
|
|
574
598
|
// in. A finding several entries reach counts under each.
|
|
575
599
|
export function rollup(findings) {
|
package/lib/sets/hidden.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { cobolCard } from '../cards.mjs';
|
|
|
4
4
|
import { decodeEbcdic, inScope, isJcl, isSource, looksEbcdic, relPath } from '../sources.mjs';
|
|
5
5
|
import { report } from '../kernel/ruleset.mjs';
|
|
6
6
|
import { treeFor, noteUnread } from '../kernel/source-tree.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { drive, loopOver } from '../kernel/shared-pass.mjs';
|
|
8
8
|
|
|
9
9
|
// Content a compiler ignores and an editor often hides, but an agent reading the source will see:
|
|
10
10
|
// the sequence area, the identification area past column 72, comment lines, and characters that
|
|
@@ -154,14 +154,14 @@ const looksBinary = (buf) => {
|
|
|
154
154
|
return odd / n > 0.05;
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
-
export function
|
|
157
|
+
export function* scanHiddenSteps(root, opts = {}) {
|
|
158
158
|
const tree = treeFor(root, opts);
|
|
159
159
|
const files = tree.list().filter(isSource).filter(inScope(opts));
|
|
160
160
|
const findings = [];
|
|
161
161
|
const stats = { filesScanned: 0, filesUnreadable: 0, filesBinary: 0, filesEbcdic: 0 };
|
|
162
162
|
// This set reads every source file in the tree as raw bytes, which is the widest reading of the
|
|
163
163
|
// nine, so it is the one most worth walking inside the guard.
|
|
164
|
-
const run =
|
|
164
|
+
const run = yield loopOver(files, (f) => {
|
|
165
165
|
let buf;
|
|
166
166
|
try { buf = tree.bytes(f); } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
167
167
|
// An EBCDIC member is decoded before any rule reads it; read as bytes it looks binary, and a
|
|
@@ -210,3 +210,5 @@ export function scanHidden(root, opts = {}) {
|
|
|
210
210
|
|
|
211
211
|
return report('hidden', { rules: HIDDEN_RULES, findings, stats, run });
|
|
212
212
|
}
|
|
213
|
+
|
|
214
|
+
export const scanHidden = (root, opts = {}) => drive(scanHiddenSteps(root, opts));
|