@portll/cobolwork 0.0.1 → 0.2.75

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.
Files changed (79) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSING.md +93 -0
  3. package/NOTICE +9 -0
  4. package/README.md +323 -3
  5. package/THIRD-PARTY-NOTICES.md +118 -0
  6. package/bin/cobolwork.mjs +354 -0
  7. package/lib/advisories.mjs +133 -0
  8. package/lib/baseline.mjs +154 -0
  9. package/lib/bms.mjs +453 -0
  10. package/lib/build.mjs +402 -0
  11. package/lib/capabilities.mjs +79 -0
  12. package/lib/cics-commands.mjs +281 -0
  13. package/lib/compliance.mjs +81 -0
  14. package/lib/consequence.mjs +139 -0
  15. package/lib/control.mjs +1515 -0
  16. package/lib/csd.mjs +77 -0
  17. package/lib/dataflow.mjs +1506 -0
  18. package/lib/diff.mjs +344 -0
  19. package/lib/explain.mjs +145 -0
  20. package/lib/gate.mjs +383 -0
  21. package/lib/index.mjs +6 -0
  22. package/lib/inventory.mjs +79 -0
  23. package/lib/jcl.mjs +478 -0
  24. package/lib/kernel/findings.mjs +94 -0
  25. package/lib/kernel/identity.mjs +216 -0
  26. package/lib/kernel/memory.mjs +217 -0
  27. package/lib/kernel/printable.mjs +6 -0
  28. package/lib/kernel/registry.mjs +79 -0
  29. package/lib/kernel/ruleset.mjs +72 -0
  30. package/lib/kernel/source-tree.mjs +159 -0
  31. package/lib/kev.mjs +27 -0
  32. package/lib/options.mjs +512 -0
  33. package/lib/packs.mjs +148 -0
  34. package/lib/parser.mjs +2055 -0
  35. package/lib/policy.mjs +163 -0
  36. package/lib/precompile-cics.mjs +169 -0
  37. package/lib/precompile.mjs +544 -0
  38. package/lib/reach.mjs +122 -0
  39. package/lib/revision.json +1 -0
  40. package/lib/revision.mjs +89 -0
  41. package/lib/sarif.mjs +222 -0
  42. package/lib/scan.mjs +272 -0
  43. package/lib/sets/build.mjs +234 -0
  44. package/lib/sets/cics.mjs +306 -0
  45. package/lib/sets/compile.mjs +187 -0
  46. package/lib/sets/copybook.mjs +174 -0
  47. package/lib/sets/flow.mjs +487 -0
  48. package/lib/sets/hidden.mjs +216 -0
  49. package/lib/sets/jcl.mjs +440 -0
  50. package/lib/sets/log.mjs +406 -0
  51. package/lib/sets/opaque.mjs +102 -0
  52. package/lib/sets/priv.mjs +322 -0
  53. package/lib/sets/recon.mjs +267 -0
  54. package/lib/sets/vendor.mjs +117 -0
  55. package/lib/sets/web.mjs +327 -0
  56. package/lib/site.mjs +164 -0
  57. package/lib/sources.mjs +156 -0
  58. package/lib/tui/app.mjs +325 -0
  59. package/lib/tui/keys.mjs +39 -0
  60. package/lib/tui/model.mjs +96 -0
  61. package/lib/tui/run.mjs +38 -0
  62. package/lib/tui/screen.mjs +59 -0
  63. package/lib/tui/terminal.mjs +46 -0
  64. package/lib/utilities.mjs +296 -0
  65. package/lib/version.mjs +15 -0
  66. package/lib/words.mjs +318 -0
  67. package/package.json +45 -6
  68. package/rules/advisories.json +264 -0
  69. package/rules/compliance-dora.json +2151 -0
  70. package/rules/compliance-ffiec.json +2134 -0
  71. package/rules/compliance-nist80053.json +2134 -0
  72. package/rules/gitleaks-mainframe.toml +57 -0
  73. package/rules/kev-ids.json +1729 -0
  74. package/rules/packs/broadcom.json +124 -0
  75. package/rules/packs/connectdirect.json +116 -0
  76. package/rules/packs/controlm.json +114 -0
  77. package/rules/system-layouts.json +28 -0
  78. package/schema/cobolwork-coverage.schema.json +65 -0
  79. 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
+ }