@portll/cobolwork 0.0.1 → 0.2.76
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/LICENSE +661 -0
- package/LICENSING.md +93 -0
- package/NOTICE +9 -0
- package/README.md +325 -3
- package/THIRD-PARTY-NOTICES.md +118 -0
- package/bin/cobolwork.mjs +354 -0
- package/lib/advisories.mjs +133 -0
- package/lib/baseline.mjs +154 -0
- package/lib/bms.mjs +453 -0
- package/lib/build.mjs +402 -0
- package/lib/capabilities.mjs +79 -0
- package/lib/cics-commands.mjs +281 -0
- package/lib/compliance.mjs +81 -0
- package/lib/consequence.mjs +139 -0
- package/lib/control.mjs +1515 -0
- package/lib/csd.mjs +77 -0
- package/lib/dataflow.mjs +1506 -0
- package/lib/diff.mjs +344 -0
- package/lib/explain.mjs +145 -0
- package/lib/gate.mjs +383 -0
- package/lib/index.mjs +6 -0
- package/lib/inventory.mjs +79 -0
- package/lib/jcl.mjs +478 -0
- package/lib/kernel/findings.mjs +94 -0
- package/lib/kernel/identity.mjs +216 -0
- package/lib/kernel/memory.mjs +217 -0
- package/lib/kernel/printable.mjs +6 -0
- package/lib/kernel/registry.mjs +79 -0
- package/lib/kernel/ruleset.mjs +72 -0
- package/lib/kernel/source-tree.mjs +159 -0
- package/lib/kev.mjs +27 -0
- package/lib/options.mjs +512 -0
- package/lib/packs.mjs +148 -0
- package/lib/parser.mjs +2055 -0
- package/lib/policy.mjs +163 -0
- package/lib/precompile-cics.mjs +169 -0
- package/lib/precompile.mjs +544 -0
- package/lib/reach.mjs +122 -0
- package/lib/revision.json +1 -0
- package/lib/revision.mjs +89 -0
- package/lib/sarif.mjs +222 -0
- package/lib/scan.mjs +272 -0
- package/lib/sets/build.mjs +234 -0
- package/lib/sets/cics.mjs +306 -0
- package/lib/sets/compile.mjs +187 -0
- package/lib/sets/copybook.mjs +174 -0
- package/lib/sets/flow.mjs +487 -0
- package/lib/sets/hidden.mjs +216 -0
- package/lib/sets/jcl.mjs +440 -0
- package/lib/sets/log.mjs +406 -0
- package/lib/sets/opaque.mjs +102 -0
- package/lib/sets/priv.mjs +322 -0
- package/lib/sets/recon.mjs +267 -0
- package/lib/sets/vendor.mjs +117 -0
- package/lib/sets/web.mjs +327 -0
- package/lib/site.mjs +164 -0
- package/lib/sources.mjs +156 -0
- package/lib/tui/app.mjs +325 -0
- package/lib/tui/keys.mjs +39 -0
- package/lib/tui/model.mjs +96 -0
- package/lib/tui/run.mjs +38 -0
- package/lib/tui/screen.mjs +59 -0
- package/lib/tui/terminal.mjs +46 -0
- package/lib/utilities.mjs +296 -0
- package/lib/version.mjs +15 -0
- package/lib/words.mjs +318 -0
- package/package.json +45 -6
- package/rules/advisories.json +264 -0
- package/rules/compliance-dora.json +2151 -0
- package/rules/compliance-ffiec.json +2134 -0
- package/rules/compliance-nist80053.json +2134 -0
- package/rules/gitleaks-mainframe.toml +57 -0
- package/rules/kev-ids.json +1729 -0
- package/rules/packs/broadcom.json +124 -0
- package/rules/packs/connectdirect.json +116 -0
- package/rules/packs/controlm.json +114 -0
- package/rules/system-layouts.json +28 -0
- package/schema/cobolwork-coverage.schema.json +65 -0
- package/schema/cobolwork.policy.schema.json +54 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Where a rule set gets its source from, as a thing that can be handed to it rather than a path it
|
|
3
|
+
// goes and reads for itself.
|
|
4
|
+
//
|
|
5
|
+
// Three reasons, in the order they matter:
|
|
6
|
+
//
|
|
7
|
+
// cost Every rule set called buildFileIndex(root) for itself, so one scan walked the
|
|
8
|
+
// tree ten times - readdirSync and realpathSync over every entry, ten times over.
|
|
9
|
+
// A tree is built once and passed.
|
|
10
|
+
//
|
|
11
|
+
// one parse The options object handed to parseSource existed in five copies, and they had
|
|
12
|
+
// drifted: lib/diff.mjs passed systemDirs: [] where every other caller passed
|
|
13
|
+
// opts.systemDirs, so diff could not resolve a system copybook that scan could.
|
|
14
|
+
// Nobody decided that. It is what copied code does.
|
|
15
|
+
//
|
|
16
|
+
// testability A rule set that takes a tree can be given one that was never on disk.
|
|
17
|
+
//
|
|
18
|
+
// CONTAINMENT. Read this before writing another adapter.
|
|
19
|
+
//
|
|
20
|
+
// SECURITY.md names reading outside the tree as its first in-scope vulnerability class: reaching
|
|
21
|
+
// outside is refused by design, so a way around that refusal is a vulnerability rather than a bug.
|
|
22
|
+
// Today that refusal is a filesystem fact - lib/parser.mjs:322 anchors on realpathSync(root), :345
|
|
23
|
+
// refuses a symlink whose realpath leaves the tree, and :448 refuses a COPY that resolves outside
|
|
24
|
+
// it. The directory tree below does not reimplement any of that; it delegates to the same
|
|
25
|
+
// buildFileIndex, so the guarantee is the one the tests already pin.
|
|
26
|
+
//
|
|
27
|
+
// An adapter that does not have a filesystem underneath it has to make that decision itself, and
|
|
28
|
+
// `contains` is where it makes it. For a tree held in memory it is key membership. For a git
|
|
29
|
+
// revision it is membership of the tree object, which cannot name a path outside itself. Anything
|
|
30
|
+
// cleverer than those two deserves the six containment tests pointed at it before it ships:
|
|
31
|
+
// test/sources.test.mjs:44, :63, :88, :101, :144 and test/review.test.mjs:114.
|
|
32
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
33
|
+
import { dirname, resolve, sep } from 'node:path';
|
|
34
|
+
import { buildFileIndex, parseSource } from '../parser.mjs';
|
|
35
|
+
import { readSource, relPath } from '../sources.mjs';
|
|
36
|
+
|
|
37
|
+
// The shape every adapter answers to. Checked rather than documented, because an adapter missing a
|
|
38
|
+
// method fails at the first rule set that happens to call it rather than at the boundary.
|
|
39
|
+
export const TREE_METHODS = ['list', 'bytes', 'text', 'contains', 'parse', 'rel'];
|
|
40
|
+
|
|
41
|
+
export function validateTree(tree) {
|
|
42
|
+
if (!tree || typeof tree !== 'object') {
|
|
43
|
+
throw Object.assign(new Error('a source tree is required'), { code: 'ETREE' });
|
|
44
|
+
}
|
|
45
|
+
const missing = TREE_METHODS.filter((m) => typeof tree[m] !== 'function');
|
|
46
|
+
if (missing.length) {
|
|
47
|
+
throw Object.assign(new Error(`source tree is missing: ${missing.join(', ')}`), { code: 'ETREE', missing });
|
|
48
|
+
}
|
|
49
|
+
return tree;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Whether a path is still under root once every link in it is followed.
|
|
53
|
+
export function resolvesInside(root, p) {
|
|
54
|
+
try {
|
|
55
|
+
const top = realpathSync(root);
|
|
56
|
+
const real = realpathSync(p);
|
|
57
|
+
return real === top || real.startsWith(top + sep);
|
|
58
|
+
} catch { return false; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A directory on disk. Behaviour is identical to what every rule set did for itself; the only
|
|
62
|
+
// difference is that the walk happens once.
|
|
63
|
+
export function directoryTree(root, opts = {}) {
|
|
64
|
+
const idx = buildFileIndex(root);
|
|
65
|
+
const systemDirs = opts.systemDirs || [];
|
|
66
|
+
const top = resolve(root);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
kind: 'directory',
|
|
70
|
+
root,
|
|
71
|
+
// The index itself, for the two callers that need more than a list: the copybook set reads
|
|
72
|
+
// copyDirs, and the inventory reports on unreadable directories and symlinks.
|
|
73
|
+
index: idx,
|
|
74
|
+
list: () => [...idx.index.values()].sort(),
|
|
75
|
+
bytes: (p) => readFileSync(p),
|
|
76
|
+
text: (p) => readSource(p),
|
|
77
|
+
rel: (p) => relPath(root, p),
|
|
78
|
+
// The same prefix test buildFileIndex applies while walking. It is not the containment
|
|
79
|
+
// mechanism - the walk is, and it resolves symlinks before testing - it is the answer to
|
|
80
|
+
// "is this path one of mine" for a caller holding a path from somewhere else.
|
|
81
|
+
contains: (p) => { const r = resolve(p); return r === top || r.startsWith(top + sep); },
|
|
82
|
+
// One parse configuration, in one place. `text` is optional: a caller that has already read
|
|
83
|
+
// the source passes it rather than reading twice.
|
|
84
|
+
parse: (file, text) => parseSource(text ?? readSource(file).text, file, {
|
|
85
|
+
format: 'auto',
|
|
86
|
+
includeDirs: idx.copyDirs,
|
|
87
|
+
fileIndex: idx.index,
|
|
88
|
+
mainDir: dirname(file),
|
|
89
|
+
copyFormat: 'auto',
|
|
90
|
+
systemDirs,
|
|
91
|
+
}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A tree that was never on disk, for tests. `files` maps a path to its contents, as a string or a
|
|
96
|
+
// Buffer. Paths are used exactly as given, so a test reads the way it writes.
|
|
97
|
+
//
|
|
98
|
+
// It cannot parse. parseSource resolves COPY statements against real directories and reads them
|
|
99
|
+
// with readSource, so a program with copybooks needs a filesystem underneath it. A rule set that
|
|
100
|
+
// only reads text - hidden, recon, vendor, jcl, build - works against this; one that parses does
|
|
101
|
+
// not, and says so rather than parsing something unexpected.
|
|
102
|
+
export function memoryTree(files, { root = '/memory' } = {}) {
|
|
103
|
+
const store = new Map(Object.entries(files).map(([p, v]) => [p, Buffer.isBuffer(v) ? v : Buffer.from(v, 'latin1')]));
|
|
104
|
+
return {
|
|
105
|
+
kind: 'memory',
|
|
106
|
+
root,
|
|
107
|
+
index: { index: store, copyDirs: [], unreadableDirs: [], symlinks: { followed: 0, outside: 0, broken: 0 } },
|
|
108
|
+
list: () => [...store.keys()].sort(),
|
|
109
|
+
bytes: (p) => {
|
|
110
|
+
const b = store.get(p);
|
|
111
|
+
if (!b) throw Object.assign(new Error(`no such file in this tree: ${p}`), { code: 'ENOENT' });
|
|
112
|
+
return b;
|
|
113
|
+
},
|
|
114
|
+
text: (p) => {
|
|
115
|
+
const b = store.get(p);
|
|
116
|
+
if (!b) throw Object.assign(new Error(`no such file in this tree: ${p}`), { code: 'ENOENT' });
|
|
117
|
+
return { text: b.toString('latin1'), encoding: 'latin1' };
|
|
118
|
+
},
|
|
119
|
+
rel: (p) => String(p).replace(/\\/g, '/').replace(new RegExp('^' + root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/?'), ''),
|
|
120
|
+
// Containment by construction: a path this tree does not hold is a path outside it.
|
|
121
|
+
contains: (p) => store.has(p),
|
|
122
|
+
parse: () => {
|
|
123
|
+
throw Object.assign(
|
|
124
|
+
new Error('a memory tree cannot parse: COPY resolution reads directories from disk'),
|
|
125
|
+
{ code: 'ETREEPARSE' },
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// What a rule set calls. Given a tree it uses it; given none it builds the directory one, so every
|
|
132
|
+
// existing caller keeps working unchanged.
|
|
133
|
+
export const treeFor = (root, opts = {}) => (opts.tree ? validateTree(opts.tree) : directoryTree(root, opts));
|
|
134
|
+
|
|
135
|
+
// Why a file did not contribute what it should have.
|
|
136
|
+
//
|
|
137
|
+
// Eighteen catch sites discarded the error object entirely and incremented a counter. "3 files
|
|
138
|
+
// unreadable" over a hundred thousand names nothing to go and look at, and it flattens four
|
|
139
|
+
// different situations into one number: a permission denied, a file deleted mid-scan, a program
|
|
140
|
+
// the parser could not make sense of, and this code being wrong.
|
|
141
|
+
//
|
|
142
|
+
// The last one is not hypothetical. Routing reads through the source tree left one helper without
|
|
143
|
+
// a tree in scope; its `catch { continue }` swallowed the ReferenceError, the rule set returned an
|
|
144
|
+
// empty program list, and four tests failed with nothing on record to say why. The error had a
|
|
145
|
+
// name the whole time and nobody kept it.
|
|
146
|
+
const reasonOf = (e) => (e && (e.code || e.name)) || 'unknown';
|
|
147
|
+
|
|
148
|
+
// Read failed: the bytes never arrived.
|
|
149
|
+
export function noteUnread(stats, tree, file, err) {
|
|
150
|
+
stats.filesUnreadable = (stats.filesUnreadable || 0) + 1;
|
|
151
|
+
(stats.unreadable ||= []).push(`${tree.rel(file)}: ${reasonOf(err)}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Read succeeded and the parse did not. A different thing, and counted as one: the file was read,
|
|
155
|
+
// so it is not unreadable, and a scan that says otherwise understates what it managed to look at.
|
|
156
|
+
export function noteUnparsed(stats, tree, file, err) {
|
|
157
|
+
stats.filesUnparsed = (stats.filesUnparsed || 0) + 1;
|
|
158
|
+
(stats.unparsed ||= []).push(`${tree.rel(file)}: ${reasonOf(err)}`);
|
|
159
|
+
}
|
package/lib/kev.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// CISA's Known Exploited Vulnerabilities catalogue, as identifiers. One question is asked of it -
|
|
3
|
+
// is this CVE known to be exploited in the wild - and the answer changes a finding from a
|
|
4
|
+
// theoretical defect into an observed one, which is the difference between a ticket and a page.
|
|
5
|
+
//
|
|
6
|
+
// Only the identifiers are kept. The full catalogue is 1.7MB of prose to answer a set-membership
|
|
7
|
+
// question, and this repository has no dependencies and intends to stay small.
|
|
8
|
+
//
|
|
9
|
+
// It is also the clearest example of why a feed exists at all: this file is stale the week after
|
|
10
|
+
// it is written, and a scanner that reports against a two-year-old KEV snapshot is reporting
|
|
11
|
+
// against nothing. Refresh with `node diag/refresh-kev.mjs`.
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
export const KEV = JSON.parse(readFileSync(new URL('../rules/kev-ids.json', import.meta.url), 'utf8'));
|
|
15
|
+
|
|
16
|
+
const IDS = new Set(KEV.cves);
|
|
17
|
+
|
|
18
|
+
export const isKnownExploited = (cve) => IDS.has(String(cve).toUpperCase());
|
|
19
|
+
|
|
20
|
+
// CISA publishes roughly weekly. A snapshot older than this is reported alongside any finding that
|
|
21
|
+
// depended on it, because "not in KEV" from a stale file is not an answer.
|
|
22
|
+
const STALE_AFTER_DAYS = 30;
|
|
23
|
+
|
|
24
|
+
export function kevAge(now = new Date()) {
|
|
25
|
+
const days = Math.floor((now - new Date(KEV.dateReleased + 'T00:00:00Z')) / 86400000);
|
|
26
|
+
return { days, stale: days > STALE_AFTER_DAYS, catalogVersion: KEV.catalogVersion, dateReleased: KEV.dateReleased };
|
|
27
|
+
}
|