@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,487 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import { analyze } from '../dataflow.mjs';
|
|
3
|
+
import { FLOW_MODEL, SCHEMA_VERSION, TOOL_VERSION } from '../version.mjs';
|
|
4
|
+
import { finish, withMeta, sortFindings } from '../kernel/findings.mjs';
|
|
5
|
+
|
|
6
|
+
export const RULES = {
|
|
7
|
+
'argv-or-env-to-os-command': { sev: 'crit', evidence: 'path', cwe: 'CWE-78', text: 'Command-line or environment input reaches an operating-system command routine' },
|
|
8
|
+
'cics-terminal-to-os-command': { sev: 'crit', evidence: 'path', cwe: 'CWE-78', text: 'Terminal input reaches an operating-system command routine' },
|
|
9
|
+
'cics-web-to-os-command': { sev: 'crit', evidence: 'path', cwe: 'CWE-78', text: 'Web input reaches an operating-system command routine' },
|
|
10
|
+
'file-record-to-os-command': { sev: 'high', evidence: 'path', cwe: 'CWE-78', text: 'A file record reaches an operating-system command routine' },
|
|
11
|
+
'database-to-os-command': { sev: 'high', evidence: 'path', cwe: 'CWE-78', text: 'A database value reaches an operating-system command routine' },
|
|
12
|
+
'argv-or-env-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'Command-line or environment input reaches dynamic SQL' },
|
|
13
|
+
'cics-terminal-to-dynamic-sql': { sev: 'crit', evidence: 'path', cwe: 'CWE-89', text: 'Terminal input reaches dynamic SQL' },
|
|
14
|
+
'cics-web-to-dynamic-sql': { sev: 'crit', evidence: 'path', cwe: 'CWE-89', text: 'Web input reaches dynamic SQL' },
|
|
15
|
+
'file-record-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'A file record reaches dynamic SQL' },
|
|
16
|
+
'database-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'A database value reaches dynamic SQL' },
|
|
17
|
+
'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' },
|
|
18
|
+
'cics-terminal-to-cics-dynamic-transfer': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Terminal input decides which program CICS transfers to' },
|
|
19
|
+
'cics-web-to-cics-dynamic-transfer': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Web input decides which program CICS transfers to' },
|
|
20
|
+
'file-record-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A file record decides which program CICS transfers to' },
|
|
21
|
+
'database-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A database value decides which program CICS transfers to' },
|
|
22
|
+
'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' },
|
|
23
|
+
'cics-terminal-to-dynamic-program-load': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Terminal input decides which program is called' },
|
|
24
|
+
'cics-web-to-dynamic-program-load': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Web input decides which program is called' },
|
|
25
|
+
'file-record-to-dynamic-program-load': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A file record decides which program is called' },
|
|
26
|
+
'database-to-dynamic-program-load': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A database value decides which program is called' },
|
|
27
|
+
// A program opening the file its own user named acts with that user's authority, which is every
|
|
28
|
+
// one read in the 500-repository corpus; it escalates only where the estate says the entry that
|
|
29
|
+
// starts it runs privileged.
|
|
30
|
+
'argv-or-env-to-dynamic-file-path': { sev: 'low', whenPrivileged: 'high', evidence: 'path', cwe: 'CWE-73', text: 'Command-line or environment input decides which file is opened' },
|
|
31
|
+
'cics-terminal-to-dynamic-file-path': { sev: 'high', evidence: 'path', cwe: 'CWE-73', text: 'Terminal input decides which file is opened' },
|
|
32
|
+
'cics-web-to-dynamic-file-path': { sev: 'high', evidence: 'path', cwe: 'CWE-73', text: 'Web input decides which file is opened' },
|
|
33
|
+
'file-record-to-dynamic-file-path': { sev: 'med', evidence: 'path', cwe: 'CWE-73', text: 'A file record decides which file is opened' },
|
|
34
|
+
'database-to-dynamic-file-path': { sev: 'med', evidence: 'path', cwe: 'CWE-73', text: 'A database value decides which file is opened' },
|
|
35
|
+
// A JCL step is the entry point above the program. PARM is the mainframe's argv, and in-stream
|
|
36
|
+
// data is a payload sitting in the repository: both are edited by anyone who can commit, so they
|
|
37
|
+
// are untrusted in the same sense ACCEPT FROM COMMAND-LINE is, and carry the same severities.
|
|
38
|
+
'jcl-parm-to-os-command': { sev: 'crit', evidence: 'path', cwe: 'CWE-78', text: 'A JCL PARM reaches an operating-system command routine' },
|
|
39
|
+
'jcl-parm-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'A JCL PARM reaches dynamic SQL' },
|
|
40
|
+
'jcl-parm-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A JCL PARM decides which program CICS transfers to' },
|
|
41
|
+
'jcl-parm-to-dynamic-program-load': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A JCL PARM decides which program is called' },
|
|
42
|
+
'jcl-parm-to-dynamic-file-path': { sev: 'high', evidence: 'path', cwe: 'CWE-73', text: 'A JCL PARM decides which file is opened' },
|
|
43
|
+
'jcl-instream-to-os-command': { sev: 'crit', evidence: 'path', cwe: 'CWE-78', text: 'In-stream job data reaches an operating-system command routine' },
|
|
44
|
+
'jcl-instream-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'In-stream job data reaches dynamic SQL' },
|
|
45
|
+
'jcl-instream-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'In-stream job data decides which program CICS transfers to' },
|
|
46
|
+
'jcl-instream-to-dynamic-program-load': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'In-stream job data decides which program is called' },
|
|
47
|
+
'jcl-instream-to-dynamic-file-path': { sev: 'high', evidence: 'path', cwe: 'CWE-73', text: 'In-stream job data decides which file is opened' },
|
|
48
|
+
// The internal reader runs what it is given as a job, under the authority of whoever submitted
|
|
49
|
+
// it - for a CICS program, the region. Input reaching it is the platform's command injection. A
|
|
50
|
+
// file record or database value reaching it is medium rather than high: reading JCL skeletons
|
|
51
|
+
// from a dataset and submitting them is how an ordinary job submitter works, and the exposure is
|
|
52
|
+
// whoever can write that dataset.
|
|
53
|
+
'argv-or-env-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'Command-line or environment input is written into a job the internal reader submits' },
|
|
54
|
+
'cics-terminal-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'Terminal input is written into a job the internal reader submits' },
|
|
55
|
+
'cics-web-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'Web input is written into a job the internal reader submits' },
|
|
56
|
+
'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' },
|
|
57
|
+
'database-to-internal-reader': { sev: 'med', evidence: 'path', cwe: 'CWE-77', text: 'A database value is written into a job the internal reader submits' },
|
|
58
|
+
'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' },
|
|
59
|
+
'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' },
|
|
60
|
+
// The CICS application API as an attack surface. CICS escapes nothing it sends and validates no
|
|
61
|
+
// name it is given, so the program owns all of it. No other scanner reads these commands.
|
|
62
|
+
'cics-web-to-web-response': { sev: 'high', evidence: 'path', cwe: 'CWE-79', text: 'Web input is written into the response this program returns' },
|
|
63
|
+
'cics-terminal-to-web-response': { sev: 'high', evidence: 'path', cwe: 'CWE-79', text: 'Terminal input is written into the response this program returns' },
|
|
64
|
+
'file-record-to-web-response': { sev: 'med', evidence: 'path', cwe: 'CWE-79', text: 'A file record is written into the response this program returns' },
|
|
65
|
+
'database-to-web-response': { sev: 'med', evidence: 'path', cwe: 'CWE-79', text: 'A database value is written into the response this program returns' },
|
|
66
|
+
'cics-web-to-http-header': { sev: 'high', evidence: 'path', cwe: 'CWE-113', text: 'Web input is written into a header of the response' },
|
|
67
|
+
'cics-terminal-to-http-header': { sev: 'high', evidence: 'path', cwe: 'CWE-113', text: 'Terminal input is written into a header of the response' },
|
|
68
|
+
'file-record-to-http-header': { sev: 'med', evidence: 'path', cwe: 'CWE-113', text: 'A file record is written into a header of the response' },
|
|
69
|
+
'database-to-http-header': { sev: 'med', evidence: 'path', cwe: 'CWE-113', text: 'A database value is written into a header of the response' },
|
|
70
|
+
'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' },
|
|
71
|
+
'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' },
|
|
72
|
+
'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' },
|
|
73
|
+
'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' },
|
|
74
|
+
'cics-web-to-queue-name': { sev: 'high', evidence: 'path', cwe: 'CWE-99', text: 'Web input names the queue a command reads, writes or deletes' },
|
|
75
|
+
'cics-terminal-to-queue-name': { sev: 'high', evidence: 'path', cwe: 'CWE-99', text: 'Terminal input names the queue a command reads, writes or deletes' },
|
|
76
|
+
'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' },
|
|
77
|
+
'database-to-queue-name': { sev: 'med', evidence: 'path', cwe: 'CWE-99', text: 'A database value names the queue a command reads, writes or deletes' },
|
|
78
|
+
// Where a statement reads or writes, decided by input from outside. Without SSRANGE, which is not
|
|
79
|
+
// the Enterprise COBOL default, an index out of range reads or writes the storage beside the
|
|
80
|
+
// table; with it, the program abends. File records and database values are not followed here:
|
|
81
|
+
// in batch COBOL nearly every subscript descends from one, and that volume is unmeasured.
|
|
82
|
+
'argv-or-env-to-subscript': { sev: 'high', evidence: 'path', cwe: 'CWE-129', text: 'Command-line or environment input is used as a subscript' },
|
|
83
|
+
'cics-terminal-to-subscript': { sev: 'high', evidence: 'path', cwe: 'CWE-129', text: 'Terminal input is used as a subscript' },
|
|
84
|
+
'cics-web-to-subscript': { sev: 'high', evidence: 'path', cwe: 'CWE-129', text: 'Web input is used as a subscript' },
|
|
85
|
+
'jcl-parm-to-subscript': { sev: 'high', evidence: 'path', cwe: 'CWE-129', text: 'A JCL PARM is used as a subscript' },
|
|
86
|
+
'jcl-instream-to-subscript': { sev: 'high', evidence: 'path', cwe: 'CWE-129', text: 'In-stream job data is used as a subscript' },
|
|
87
|
+
'argv-or-env-to-reference-modification': { sev: 'high', evidence: 'path', cwe: 'CWE-1285', text: 'Command-line or environment input decides where a reference modification starts or how long it is' },
|
|
88
|
+
'cics-terminal-to-reference-modification': { sev: 'high', evidence: 'path', cwe: 'CWE-1285', text: 'Terminal input decides where a reference modification starts or how long it is' },
|
|
89
|
+
'cics-web-to-reference-modification': { sev: 'high', evidence: 'path', cwe: 'CWE-1285', text: 'Web input decides where a reference modification starts or how long it is' },
|
|
90
|
+
'jcl-parm-to-reference-modification': { sev: 'high', evidence: 'path', cwe: 'CWE-1285', text: 'A JCL PARM decides where a reference modification starts or how long it is' },
|
|
91
|
+
'jcl-instream-to-reference-modification': { sev: 'high', evidence: 'path', cwe: 'CWE-1285', text: 'In-stream job data decides where a reference modification starts or how long it is' },
|
|
92
|
+
'argv-or-env-to-occurs-depending-count': { sev: 'high', evidence: 'path', cwe: 'CWE-1284', text: 'Command-line or environment input decides how many entries an OCCURS DEPENDING ON table holds' },
|
|
93
|
+
'cics-terminal-to-occurs-depending-count': { sev: 'high', evidence: 'path', cwe: 'CWE-1284', text: 'Terminal input decides how many entries an OCCURS DEPENDING ON table holds' },
|
|
94
|
+
'cics-web-to-occurs-depending-count': { sev: 'high', evidence: 'path', cwe: 'CWE-1284', text: 'Web input decides how many entries an OCCURS DEPENDING ON table holds' },
|
|
95
|
+
'jcl-parm-to-occurs-depending-count': { sev: 'high', evidence: 'path', cwe: 'CWE-1284', text: 'A JCL PARM decides how many entries an OCCURS DEPENDING ON table holds' },
|
|
96
|
+
'jcl-instream-to-occurs-depending-count': { sev: 'high', evidence: 'path', cwe: 'CWE-1284', text: 'In-stream job data decides how many entries an OCCURS DEPENDING ON table holds' },
|
|
97
|
+
'argv-or-env-to-loop-bound': { sev: 'high', evidence: 'path', cwe: 'CWE-129', text: 'Command-line or environment input decides how far a loop that subscripts a table runs' },
|
|
98
|
+
'cics-terminal-to-loop-bound': { sev: 'high', evidence: 'path', cwe: 'CWE-129', text: 'Terminal input decides how far a loop that subscripts a table runs' },
|
|
99
|
+
'cics-web-to-loop-bound': { sev: 'high', evidence: 'path', cwe: 'CWE-129', text: 'Web input decides how far a loop that subscripts a table runs' },
|
|
100
|
+
'jcl-parm-to-loop-bound': { sev: 'high', evidence: 'path', cwe: 'CWE-129', text: 'A JCL PARM decides how far a loop that subscripts a table runs' },
|
|
101
|
+
'jcl-instream-to-loop-bound': { sev: 'high', evidence: 'path', cwe: 'CWE-129', text: 'In-stream job data decides how far a loop that subscripts a table runs' },
|
|
102
|
+
// Bytes that are not decimal digits in a zoned or packed operand abend the program: an outage of
|
|
103
|
+
// the transaction or the job, not a corruption, and a caller at a terminal or a browser can cause
|
|
104
|
+
// one at will. Whoever edits a job or a command line abends only their own run.
|
|
105
|
+
'cics-terminal-to-arithmetic': { sev: 'med', evidence: 'path', cwe: 'CWE-1287', text: 'Terminal input reaches arithmetic on a decimal field, where bytes that are not digits abend the program' },
|
|
106
|
+
// A log line is a record, and a reader that prints or converts it breaks lines where the data says
|
|
107
|
+
// to: a modified 3270 client or a web request can put X'15' or CR LF in a field. Low, because it
|
|
108
|
+
// misleads whoever reads the log rather than changing what the program does.
|
|
109
|
+
'cics-terminal-to-log': { sev: 'low', evidence: 'path', cwe: 'CWE-117', text: 'Terminal input is written to a log unchecked, where control characters in it can start a line the program did not write' },
|
|
110
|
+
'cics-web-to-log': { sev: 'low', evidence: 'path', cwe: 'CWE-117', text: 'Web request data is written to a log unchecked, where control characters in it can start a line the program did not write' },
|
|
111
|
+
// A response code in a reply tells the client which failure it caused and something of the system
|
|
112
|
+
// behind the program, which is what an attacker probing it wants.
|
|
113
|
+
'system-response-to-web-response': { sev: 'low', evidence: 'path', cwe: 'CWE-209', text: 'A response code or SQL error the system set is sent in a web response' },
|
|
114
|
+
'system-response-to-http-header': { sev: 'low', evidence: 'path', cwe: 'CWE-209', text: 'A response code or SQL error the system set is sent in a response header' },
|
|
115
|
+
// A field the program filled and the map protects, read back and used to choose a record: the 3270
|
|
116
|
+
// hidden form field. The terminal enforces PROT and ASKIP, CICS does not (NetSPI 'Conquering CICS',
|
|
117
|
+
// ways 3 and 4). No check lowers it: a key that is well formed is still someone else's key.
|
|
118
|
+
// Medium, because whether a key the list did not offer reaches a record the user could not
|
|
119
|
+
// already choose depends on how the list was scoped, which the code does not say.
|
|
120
|
+
'cics-protected-field-to-record-key': { sev: 'med', evidence: 'path', cwe: 'CWE-639', text: 'A field the map protects, which a modified 3270 client can change, decides which record the program reads or browses from' },
|
|
121
|
+
// Where the record chosen is one the program then rewrites or deletes, the question is no longer
|
|
122
|
+
// what the user may see but what they may change, and an unscoped list is no defence.
|
|
123
|
+
'cics-protected-field-to-record-update': { sev: 'high', evidence: 'path', cwe: 'CWE-639', text: 'A field the map protects, which a modified 3270 client can change, decides which record the program changes or deletes' },
|
|
124
|
+
'cics-web-to-arithmetic': { sev: 'med', evidence: 'path', cwe: 'CWE-1287', text: 'Web input reaches arithmetic on a decimal field, where bytes that are not digits abend the program' },
|
|
125
|
+
'argv-or-env-to-arithmetic': { sev: 'low', evidence: 'path', cwe: 'CWE-1287', text: 'Command-line or environment input reaches arithmetic on a decimal field, where bytes that are not digits abend the program' },
|
|
126
|
+
'jcl-parm-to-arithmetic': { sev: 'low', evidence: 'path', cwe: 'CWE-1287', text: 'A JCL PARM reaches arithmetic on a decimal field, where bytes that are not digits abend the program' },
|
|
127
|
+
'jcl-instream-to-arithmetic': { sev: 'low', evidence: 'path', cwe: 'CWE-1287', text: 'In-stream job data reaches arithmetic on a decimal field, where bytes that are not digits abend the program' },
|
|
128
|
+
// Exfiltration: data at rest leaving through a channel the program opened itself. High for HTTP
|
|
129
|
+
// and sockets, where the far end is named by the program; low for a queue, which is the ordinary
|
|
130
|
+
// way mainframe systems hand data to each other and is listed so the route is visible.
|
|
131
|
+
'database-to-outbound-http': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A database value is sent to a remote server over HTTP' },
|
|
132
|
+
'file-record-to-outbound-http': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A file record is sent to a remote server over HTTP' },
|
|
133
|
+
'database-to-socket-send': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A database value is written to a network socket' },
|
|
134
|
+
'file-record-to-socket-send': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A file record is written to a network socket' },
|
|
135
|
+
'database-to-message-queue': { sev: 'low', evidence: 'path', cwe: 'CWE-201', text: 'A database value is put on a message queue' },
|
|
136
|
+
'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' },
|
|
137
|
+
'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' },
|
|
138
|
+
'file-record-to-message-queue': { sev: 'low', evidence: 'path', cwe: 'CWE-201', text: 'A file record is put on a message queue' },
|
|
139
|
+
// The CALL interface itself, from the same bound pairs the paths above cross. No input has to
|
|
140
|
+
// reach these: a callee that declares more than it is passed reaches past it on every call.
|
|
141
|
+
'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' },
|
|
142
|
+
'call-parameter-exceeds-argument': { sev: 'low', evidence: 'construct', cwe: 'CWE-628', text: 'A called program declares a parameter longer than the field its caller passes, reaching into the fields beside it' },
|
|
143
|
+
// Where the tree holds the estate's entries - a CSD, job steps - a program none of them reaches is
|
|
144
|
+
// one no route in this tree starts. It asserts no defect: it may be started from elsewhere, or be
|
|
145
|
+
// dead, and either is worth knowing before its findings are triaged.
|
|
146
|
+
'program-without-entry': { sev: 'info', evidence: 'context', cwe: null, text: 'Nothing in this tree starts this program: no transaction, job step or program reaches it' },
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// `impact` names who can do what once a route reaches the sink, so a reader can tell a hole to close
|
|
150
|
+
// from one to note; `remedy` is the standard fix, one per sink. Both ride with every finding as
|
|
151
|
+
// ruleImpact/ruleRemedy. Composed from the source's actor and the sink's guidance rather than
|
|
152
|
+
// written out for each path rule.
|
|
153
|
+
const WHO = {
|
|
154
|
+
'argv-or-env': "Whoever sets the program's command line or environment",
|
|
155
|
+
'cics-terminal': 'A terminal user',
|
|
156
|
+
'cics-web': 'A web caller',
|
|
157
|
+
'file-record': 'Whoever can write the file record',
|
|
158
|
+
'database': 'Whoever can write the database row',
|
|
159
|
+
'jcl-parm': "Whoever can edit the job's PARM",
|
|
160
|
+
'jcl-instream': "Whoever can edit the job's in-stream data",
|
|
161
|
+
// A protected 3270 field is a value a terminal user controls with a modified client.
|
|
162
|
+
'cics-protected-field': 'A terminal user',
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const SINK = {
|
|
166
|
+
'os-command': {
|
|
167
|
+
clause: "runs an arbitrary operating-system command with the program's authority",
|
|
168
|
+
remedy: "Build the command only from fixed literals; never place input in the argument of CALL 'SYSTEM' or BPXWDYN. If it must vary, choose from an allow-list of known commands",
|
|
169
|
+
},
|
|
170
|
+
'dynamic-sql': {
|
|
171
|
+
clause: 'changes the meaning of the SQL statement',
|
|
172
|
+
remedy: 'Use a static statement with host variables or parameter markers; never concatenate input into EXECUTE IMMEDIATE or PREPARE text',
|
|
173
|
+
},
|
|
174
|
+
'cics-dynamic-transfer': {
|
|
175
|
+
clause: 'chooses which program the region transfers to',
|
|
176
|
+
remedy: 'Resolve the program name from a fixed table of literals, never from input, a record or the communication area',
|
|
177
|
+
},
|
|
178
|
+
'dynamic-program-load': {
|
|
179
|
+
clause: 'chooses which program is called',
|
|
180
|
+
remedy: 'Resolve the called program name from a fixed table of literals, never from input or a record',
|
|
181
|
+
},
|
|
182
|
+
'dynamic-file-path': {
|
|
183
|
+
clause: 'chooses which dataset the program opens',
|
|
184
|
+
remedy: 'Map input to a fixed set of DDs or dataset names; do not build the ASSIGN or dynamic-allocation name from input',
|
|
185
|
+
},
|
|
186
|
+
'internal-reader': {
|
|
187
|
+
clause: "submits an arbitrary job, for a CICS program under the region's authority",
|
|
188
|
+
remedy: 'Build submitted JCL from fixed skeletons; keep input out of the job stream, or validate it against a strict allow-list',
|
|
189
|
+
},
|
|
190
|
+
'web-response': {
|
|
191
|
+
clause: 'places chosen markup or script in the response this program returns',
|
|
192
|
+
remedy: 'Encode input for the output context before WEB SEND or SEND TEXT; do not emit it raw',
|
|
193
|
+
},
|
|
194
|
+
'http-header': {
|
|
195
|
+
clause: 'injects header content, splitting or forging the response header',
|
|
196
|
+
remedy: 'Reject CR and LF and validate the value against an allow-list before WRITE HTTPHEADER',
|
|
197
|
+
},
|
|
198
|
+
'outbound-host': {
|
|
199
|
+
clause: 'sends data at rest to a host the program connects to',
|
|
200
|
+
remedy: 'Confirm the destination and the data classification are intended, and restrict who can write the source record',
|
|
201
|
+
},
|
|
202
|
+
'socket-send': {
|
|
203
|
+
clause: 'sends data at rest over a socket the program opened',
|
|
204
|
+
remedy: 'Confirm the destination and data classification are intended, and restrict who can write the source record',
|
|
205
|
+
},
|
|
206
|
+
'queue-name': {
|
|
207
|
+
clause: 'chooses the queue the message is sent to',
|
|
208
|
+
remedy: 'Select the queue from a fixed table, not from input or a record',
|
|
209
|
+
},
|
|
210
|
+
'message-queue': {
|
|
211
|
+
clause: 'sends data at rest onto a message queue',
|
|
212
|
+
remedy: 'Confirm the queue and data classification are intended; queues are how systems hand data on, so review the destination',
|
|
213
|
+
},
|
|
214
|
+
'extrapartition-queue': {
|
|
215
|
+
clause: 'sends data at rest to an extrapartition queue that leaves the region',
|
|
216
|
+
remedy: "Confirm the queue's destination and the data classification are intended",
|
|
217
|
+
},
|
|
218
|
+
'subscript': {
|
|
219
|
+
clause: 'indexes a table past its bound into adjacent storage, or abends with SSRANGE',
|
|
220
|
+
remedy: 'Bound-check the value against the table size before using it as a subscript, or compile SSRANGE',
|
|
221
|
+
},
|
|
222
|
+
'reference-modification': {
|
|
223
|
+
clause: 'sets a reference-modification start or length past the field into adjacent storage',
|
|
224
|
+
remedy: 'Bound-check the offset and length against the field before the reference modification',
|
|
225
|
+
},
|
|
226
|
+
'occurs-depending-count': {
|
|
227
|
+
clause: 'sets an OCCURS DEPENDING ON count outside its declared range',
|
|
228
|
+
remedy: 'Validate the count against the OCCURS bounds before it sizes the table',
|
|
229
|
+
},
|
|
230
|
+
'loop-bound': {
|
|
231
|
+
clause: 'drives a loop past the table it subscripts',
|
|
232
|
+
remedy: 'Bound the loop counter against the table size, independent of the input',
|
|
233
|
+
},
|
|
234
|
+
'arithmetic': {
|
|
235
|
+
clause: 'puts non-numeric data into a numeric field, abending the transaction (S0C7, ASRA in CICS)',
|
|
236
|
+
remedy: 'Test the field IS NUMERIC before the arithmetic and reject or default on failure',
|
|
237
|
+
},
|
|
238
|
+
'log': {
|
|
239
|
+
clause: 'forges log lines with CR/LF, or writes a credential-shaped value to a log',
|
|
240
|
+
remedy: 'Strip control characters before DISPLAY or journal, and keep credential-shaped fields out of logs',
|
|
241
|
+
},
|
|
242
|
+
'record-key': {
|
|
243
|
+
clause: 'reads a record chosen by a value a modified 3270 client controls',
|
|
244
|
+
remedy: 'Re-derive the key from server state or authorize the access; do not trust a protected or hidden screen field as a key',
|
|
245
|
+
},
|
|
246
|
+
'record-update': {
|
|
247
|
+
clause: 'updates or deletes a record chosen by a value a modified 3270 client controls',
|
|
248
|
+
remedy: 'Re-derive the key from server state and authorize the change; do not trust a protected or hidden screen field as a key',
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
// Exfiltration over HTTP reads the same as the host case.
|
|
252
|
+
SINK['outbound-http'] = SINK['outbound-host'];
|
|
253
|
+
|
|
254
|
+
// system-response leaks internals rather than carrying input, so both its routes share one line.
|
|
255
|
+
const SYSTEM_RESPONSE = {
|
|
256
|
+
impact: 'A system diagnostic (SQLCODE, RESP) reaches the response, revealing internals to the caller',
|
|
257
|
+
remedy: 'Return a fixed message to the caller and keep SQLCODE, RESP and RESP2 out of the response',
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// The CALL-interface constructs take no input, so they are set outright, not composed.
|
|
261
|
+
const CONSTRUCT = {
|
|
262
|
+
'call-parameter-exceeds-caller-record': {
|
|
263
|
+
impact: "The callee's LINKAGE item is longer than the caller's whole argument, so the callee reads past it into unrelated storage on every call",
|
|
264
|
+
remedy: "Make the callee's LINKAGE item no larger than every caller's argument, or pass and honour an explicit length",
|
|
265
|
+
},
|
|
266
|
+
'call-parameter-exceeds-argument': {
|
|
267
|
+
impact: "The callee's LINKAGE item reaches past the argument into the fields beside it on every call, which is sometimes deliberate",
|
|
268
|
+
remedy: "Size the callee's LINKAGE item to the argument, or document that it deliberately spans the following fields",
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
for (const [id, r] of Object.entries(RULES)) {
|
|
273
|
+
if (r.evidence === 'construct') {
|
|
274
|
+
r.impact = CONSTRUCT[id].impact;
|
|
275
|
+
r.remedy = CONSTRUCT[id].remedy;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (r.evidence !== 'path') continue; // program-without-entry is context: no impact or remedy
|
|
279
|
+
const at = id.indexOf('-to-');
|
|
280
|
+
const source = id.slice(0, at), sink = id.slice(at + 4);
|
|
281
|
+
if (source === 'system-response') {
|
|
282
|
+
r.impact = SYSTEM_RESPONSE.impact;
|
|
283
|
+
r.remedy = SYSTEM_RESPONSE.remedy;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
r.impact = `${WHO[source]} ${SINK[sink].clause}`;
|
|
287
|
+
r.remedy = SINK[sink].remedy;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const DEFAULT = { sev: 'med', evidence: 'path', cwe: null, text: 'Untrusted input reaches a sensitive operation' };
|
|
291
|
+
|
|
292
|
+
// A route on which a check has run before the value is used is lowered one step and says where the
|
|
293
|
+
// check is: the check may not turn away everything it should. A route on which what the check leaves
|
|
294
|
+
// is safe for the sink - one of a list of literals, digits where digits are needed, a bound where an
|
|
295
|
+
// index is - is not a finding at all, and is listed under `checked` with the check that stops it.
|
|
296
|
+
const LOWER = { crit: 'high', high: 'med', med: 'low', low: 'info', info: 'info' };
|
|
297
|
+
|
|
298
|
+
// One graph per repository, for the reason scanAll gives: names are only unique within one.
|
|
299
|
+
function analyzeEach(root, opts) {
|
|
300
|
+
if (!opts.repos || opts.repos.length < 2) return analyze(root, opts);
|
|
301
|
+
const merged = { findings: [], constructs: [], unstarted: [], startedBy: {}, stats: {} };
|
|
302
|
+
for (const repo of opts.repos) {
|
|
303
|
+
const one = analyze(root, { ...opts, repos: [repo] });
|
|
304
|
+
merged.findings.push(...one.findings);
|
|
305
|
+
merged.constructs.push(...one.constructs);
|
|
306
|
+
merged.unstarted.push(...one.unstarted);
|
|
307
|
+
for (const [k, v] of Object.entries(one.startedBy)) if (!merged.startedBy[k]) merged.startedBy[k] = v;
|
|
308
|
+
// Counts add; lists join; a count of counts adds field by field; anything else keeps the first.
|
|
309
|
+
for (const [k, v] of Object.entries(one.stats)) {
|
|
310
|
+
const had = merged.stats[k];
|
|
311
|
+
if (typeof v === 'number') merged.stats[k] = (had || 0) + v;
|
|
312
|
+
else if (Array.isArray(v)) merged.stats[k] = [...(had || []), ...v];
|
|
313
|
+
else if (v && typeof v === 'object') { merged.stats[k] = { ...(had || {}) }; for (const [f, n] of Object.entries(v)) merged.stats[k][f] = (merged.stats[k][f] || 0) + n; }
|
|
314
|
+
else if (had == null) merged.stats[k] = v;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return merged;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function scan(root, opts = {}) {
|
|
321
|
+
const res = analyzeEach(root, opts);
|
|
322
|
+
const findings = [];
|
|
323
|
+
for (const f of res.findings) {
|
|
324
|
+
const meta = RULES[f.rule] || DEFAULT;
|
|
325
|
+
// Compiled with SSRANGE, an index out of range stops the program rather than reaching the
|
|
326
|
+
// storage beside the table: an outage rather than a corruption, one step lower. A sink no route
|
|
327
|
+
// reaches does not run, and the finding asserts no defect.
|
|
328
|
+
const base = f.unreached ? 'info' : f.ssrange ? LOWER[meta.sev] : meta.sev;
|
|
329
|
+
findings.push({
|
|
330
|
+
rule: f.rule,
|
|
331
|
+
sev: base,
|
|
332
|
+
level: f.guard ? (f.guard.stops ? 2 : 1) : 0,
|
|
333
|
+
...(f.ssrange ? { ssrange: true } : {}),
|
|
334
|
+
...(f.unreached ? { unreached: true } : {}),
|
|
335
|
+
...(f.guard ? { guard: f.guard } : {}),
|
|
336
|
+
...(f.checkElsewhere ? { checkElsewhere: f.checkElsewhere } : {}),
|
|
337
|
+
...(f.startedBy ? { startedBy: f.startedBy, ...(f.startedByMore ? { startedByMore: f.startedByMore } : {}) } : {}),
|
|
338
|
+
cwe: meta.cwe,
|
|
339
|
+
path: f.sink.file,
|
|
340
|
+
line: f.sink.line,
|
|
341
|
+
program: f.sink.program,
|
|
342
|
+
crossProgram: f.crossProgram,
|
|
343
|
+
hops: f.hops,
|
|
344
|
+
detail: `${meta.text}: ${f.source.detail} at ${f.source.file}:${f.source.line} reaches ${f.sink.detail}` +
|
|
345
|
+
(f.screen && f.source.kind !== 'cics-protected-field' ? `; it starts in ${f.screen.item}, field ${f.screen.field} of map ${f.screen.map}, which the map marks ${f.screen.marks.join(' and ')}${f.screen.declared ? '' : ' by default'}: the terminal enforces that, CICS does not, and a modified client ignores it` : '') +
|
|
346
|
+
(f.unreached ? `; no route from the program's entries reaches ${f.sink.file}:${f.sink.line}, so as the program stands it does not run` : ''),
|
|
347
|
+
...(f.screen ? { screen: f.screen } : {}),
|
|
348
|
+
// elided and dir are carried through: a reader that has to regex the marker text to learn
|
|
349
|
+
// how many hops are missing cannot tell a truncated trace from one that mentions hops.
|
|
350
|
+
trace: f.path.map(p => ({ program: p.program, item: p.item, file: p.file, via: p.via, ...(p.dir ? { dir: p.dir } : {}), ...(p.elided ? { elided: p.elided } : {}) })),
|
|
351
|
+
related: [{ path: f.source.file, line: f.source.line, detail: f.source.detail }],
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// One sink reached by many sources is one finding, not many: a report where a single statement
|
|
355
|
+
// appears six hundred times says more about the loop than about the program.
|
|
356
|
+
const perSink = new Map();
|
|
357
|
+
for (const f of findings) {
|
|
358
|
+
const key = `${f.rule}|${f.path}:${f.line}|${f.program || ''}`;
|
|
359
|
+
const held = perSink.get(key);
|
|
360
|
+
if (!held) { perSink.set(key, { ...f, sources: 1 }); continue; }
|
|
361
|
+
held.sources++;
|
|
362
|
+
// A sink is credited only as far as the least-credited source that reaches it.
|
|
363
|
+
const level = Math.min(held.level, f.level);
|
|
364
|
+
if (f.hops < held.hops) Object.assign(held, f, { sources: held.sources });
|
|
365
|
+
held.level = level;
|
|
366
|
+
}
|
|
367
|
+
findings.length = 0;
|
|
368
|
+
const checked = [];
|
|
369
|
+
for (const f of perSink.values()) {
|
|
370
|
+
const { level, ...rest } = f;
|
|
371
|
+
if (level === 2) { checked.push(rest); continue; }
|
|
372
|
+
if (level === 1) {
|
|
373
|
+
const { stops, ...guard } = rest.guard;
|
|
374
|
+
findings.push({ ...rest, sev: LOWER[rest.sev], guard, guardedFrom: rest.sev });
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
delete rest.guard;
|
|
378
|
+
findings.push(rest);
|
|
379
|
+
}
|
|
380
|
+
// One finding per CALL statement and rule, naming every argument that falls short, so a call
|
|
381
|
+
// with two short arguments is one thing to fix rather than two.
|
|
382
|
+
for (const c of res.constructs) {
|
|
383
|
+
findings.push({
|
|
384
|
+
rule: c.rule,
|
|
385
|
+
path: c.file,
|
|
386
|
+
line: c.line,
|
|
387
|
+
program: c.program,
|
|
388
|
+
...(c.sev ? { sev: c.sev } : {}),
|
|
389
|
+
detail: `${c.program} calls ${c.callee}: ${c.args.map(a => `argument ${a.position} ${a.argument} is ${a.argumentBytes} bytes (${a.bytesToEndOfRecord} to the end of its record), ${c.callee} declares ${a.parameter} as ${a.parameterBytes}`).join('; ')}${c.sev === 'low' ? `; ${c.callee} only reads past it` : ''}`,
|
|
390
|
+
related: c.args.map(a => ({ path: a.declared.file, line: a.declared.line, detail: `${c.callee} declares ${a.parameter} as ${a.parameterBytes} bytes` })),
|
|
391
|
+
arguments: c.args.map(({ declared, writesPast, ...a }) => a),
|
|
392
|
+
...(c.startedBy ? { startedBy: c.startedBy, ...(c.startedByMore ? { startedByMore: c.startedByMore } : {}) } : {}),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
for (const u of res.unstarted || []) {
|
|
396
|
+
findings.push({
|
|
397
|
+
rule: 'program-without-entry', path: u.file, line: u.line || 1, program: u.program,
|
|
398
|
+
detail: `nothing in this tree starts ${u.program}: no transaction the CSD defines runs it, no job step runs it, and no program calls, links or transfers to it or spells its name`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
const { byRule } = finish(RULES, findings, 'flow');
|
|
402
|
+
const entries = res.stats.entryPoints && res.stats.entryPoints.roots ? rollup(findings) : null;
|
|
403
|
+
sortFindings(withMeta(RULES, checked, 'flow'));
|
|
404
|
+
const report = {
|
|
405
|
+
// Its own name: a flow report dropped where a scan report is expected has three rule sets
|
|
406
|
+
// missing, and with the scan's name it read as a whole one.
|
|
407
|
+
tool: 'cobolwork-flow',
|
|
408
|
+
schemaVersion: SCHEMA_VERSION,
|
|
409
|
+
summary: {
|
|
410
|
+
findings: findings.length,
|
|
411
|
+
byRule,
|
|
412
|
+
flowModel: FLOW_MODEL,
|
|
413
|
+
toolVersion: TOOL_VERSION,
|
|
414
|
+
filesScanned: res.stats.files,
|
|
415
|
+
programsParsed: res.stats.programs,
|
|
416
|
+
filesUnreadable: res.stats.threw + (res.stats.unreadable || 0),
|
|
417
|
+
filesEbcdic: res.stats.ebcdic || 0,
|
|
418
|
+
filesOverBudget: res.stats.overBudget,
|
|
419
|
+
crossProgram: findings.filter(f => f.crossProgram).length,
|
|
420
|
+
// Routes a check stops, listed under `checked` rather than counted.
|
|
421
|
+
checked: checked.length,
|
|
422
|
+
...(res.stats.entryPoints ? { entryPoints: res.stats.entryPoints } : {}),
|
|
423
|
+
...(entries ? entries : {}),
|
|
424
|
+
...(res.stats.programsUnordered ? { programsUnordered: res.stats.programsUnordered } : {}),
|
|
425
|
+
// What a RECEIVE MAP could be read against. A map whose source is not here leaves which of its
|
|
426
|
+
// fields are protected unknown, which is a limit on the protected-field rule, not a result.
|
|
427
|
+
...(res.stats.mapsReceived ? { mapsReceived: res.stats.mapsReceived, protectedFields: res.stats.protectedFields,
|
|
428
|
+
...(res.stats.mapsNoSource || res.stats.mapsNoSymbolic ? { mapsNotRead: `${res.stats.mapsNoSource + res.stats.mapsNoSymbolic} of ${res.stats.mapsReceived} RECEIVE MAP statement(s) could not be read against their map: ${res.stats.mapsNoSource} name a map whose BMS source is not in the tree, ${res.stats.mapsNoSymbolic} have no symbolic map the program declares, so which fields they protect is not known` } : {}) } : {}),
|
|
429
|
+
nosrc: res.stats.files === 0,
|
|
430
|
+
// This set states its own coverage now. It did not, and the conformance test over the
|
|
431
|
+
// registry is what noticed: it is the largest set, it has both a byte budget and the memory
|
|
432
|
+
// guard, and it was the one set that never said whether it had read everything. scanAll
|
|
433
|
+
// happened to compute the right answer for the whole report - it ORs filesOverBudget in
|
|
434
|
+
// separately - so nothing was wrong downstream, and nothing would have been until someone
|
|
435
|
+
// called this set on its own and believed a silence.
|
|
436
|
+
coverageIncomplete: (res.stats.overBudget || 0) > 0
|
|
437
|
+
|| (res.stats.filesNotReached || 0) > 0
|
|
438
|
+
|| (res.stats.unreadable || 0) > 0
|
|
439
|
+
|| (res.stats.threw || 0) > 0
|
|
440
|
+
|| (res.stats.walksCut || 0) > 0
|
|
441
|
+
|| (res.stats.sourcesNotWalked || 0) > 0,
|
|
442
|
+
// A walk stopped at its bound, or a source never walked, may leave a path unfound.
|
|
443
|
+
...(res.stats.walksCut || res.stats.sourcesNotWalked ? {
|
|
444
|
+
walksCut: res.stats.walksCut, sourcesNotWalked: res.stats.sourcesNotWalked,
|
|
445
|
+
readInPart: `${res.stats.walksCut} source(s) stopped at the bound on one walk and ${res.stats.sourcesNotWalked} not walked once the analysis reached its own; paths from them may be missing`,
|
|
446
|
+
} : {}),
|
|
447
|
+
// A check whose route could not be followed to the end credits nothing, so these are reported unchecked.
|
|
448
|
+
...(res.stats.creditUndecided ? { creditUndecided: res.stats.creditUndecided } : {}),
|
|
449
|
+
// The internal-reader rules could not run over queues nobody declared. That is configuration
|
|
450
|
+
// rather than coverage - every file was read - and it travels the way recon's does.
|
|
451
|
+
...(res.stats.tdWritesUndecided ? {
|
|
452
|
+
setIncomplete: true,
|
|
453
|
+
notLooked: [`${res.stats.tdWritesUndecided} EXEC CICS WRITEQ TD statement(s) write to an extrapartition queue, and nothing says whether it reaches the internal reader: name the region's INTRDR DDs as internalReaderDds (or the queues as internalReaderQueues) in cobolwork.site.json`],
|
|
454
|
+
} : {}),
|
|
455
|
+
...(res.stats.unreadableFiles ? { unreadable: res.stats.unreadableFiles } : {}),
|
|
456
|
+
...(res.stats.unparsedFiles ? { unparsed: res.stats.unparsedFiles } : {}),
|
|
457
|
+
...(res.stats.filesNotReached ? { filesNotReached: res.stats.filesNotReached } : {}),
|
|
458
|
+
...(res.stats.stoppedBy ? { stoppedBy: res.stats.stoppedBy } : {}),
|
|
459
|
+
},
|
|
460
|
+
findings,
|
|
461
|
+
checked,
|
|
462
|
+
ruleText: Object.fromEntries(Object.entries(RULES).map(([k, v]) => [k, v.text])),
|
|
463
|
+
ruleCwe: Object.fromEntries(Object.entries(RULES).map(([k, v]) => [k, v.cwe]).filter(([, v]) => v)),
|
|
464
|
+
ruleImpact: Object.fromEntries(Object.entries(RULES).map(([k, v]) => [k, v.impact]).filter(([, v]) => v)),
|
|
465
|
+
ruleRemedy: Object.fromEntries(Object.entries(RULES).map(([k, v]) => [k, v.remedy]).filter(([, v]) => v)),
|
|
466
|
+
};
|
|
467
|
+
// What starts each program, for the whole scan to put on findings no other set could place. Not
|
|
468
|
+
// printed: it is the same fact every finding already carries.
|
|
469
|
+
Object.defineProperty(report, 'startedBy', { value: res.startedBy || {}, enumerable: false });
|
|
470
|
+
// Every sink and source the graph holds, reached or not, when asked for: never printed.
|
|
471
|
+
if (res.sinks || res.sources) Object.defineProperty(report, 'listed', { value: { sinks: res.sinks || [], sources: res.sources || [] }, enumerable: false });
|
|
472
|
+
return report;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Findings counted by the transaction or job that reaches them: the unit a mainframe team triages
|
|
476
|
+
// in. A finding several entries reach counts under each.
|
|
477
|
+
export function rollup(findings) {
|
|
478
|
+
const byTransaction = {};
|
|
479
|
+
const byJob = {};
|
|
480
|
+
for (const f of findings) {
|
|
481
|
+
for (const e of f.startedBy || []) {
|
|
482
|
+
if (e.transaction) byTransaction[e.transaction] = (byTransaction[e.transaction] || 0) + 1;
|
|
483
|
+
if (e.job !== undefined) { const k = `${e.job || '?'} ${e.step || '(unnamed)'}`; byJob[k] = (byJob[k] || 0) + 1; }
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return { byTransaction, byJob };
|
|
487
|
+
}
|