@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/sarif.mjs
CHANGED
|
@@ -45,13 +45,23 @@ const DECLARED = new Map([...Object.entries(ALL_RULES), ...Object.entries(DIFF_R
|
|
|
45
45
|
const SET_OF = new Map();
|
|
46
46
|
for (const s of REGISTRY) for (const id of Object.keys(s.rules)) SET_OF.set(id, s.name);
|
|
47
47
|
|
|
48
|
+
// A related entry without a path is an advisory the finding rests on, not a place in the tree.
|
|
49
|
+
const located = (f) => (f.related || []).filter((r) => r.path);
|
|
50
|
+
|
|
51
|
+
const cweTaxon = (cwe) => /^CWE-(\d+)$/.exec(cwe || '')?.[1] ?? null;
|
|
52
|
+
|
|
53
|
+
// The CWE taxonomy is run.taxonomies[0] whenever any rule names a CWE.
|
|
54
|
+
const CWE_COMPONENT = { name: 'CWE', index: 0 };
|
|
55
|
+
|
|
48
56
|
// SARIF's level cannot say what kind of claim a result is, so evidence rides in the property bags.
|
|
49
57
|
const descriptorFor = (report) => (id) => {
|
|
50
58
|
const properties = {
|
|
51
59
|
...(report.ruleCwe?.[id] ? { cwe: report.ruleCwe[id] } : {}),
|
|
52
60
|
...(report.ruleEvidence?.[id] ? { evidence: report.ruleEvidence[id] } : {}),
|
|
61
|
+
...(report.ruleCompliance?.[id] ? { compliance: report.ruleCompliance[id] } : {}),
|
|
53
62
|
...(SECURITY_SEVERITY[DECLARED.get(id)] ? { 'security-severity': SECURITY_SEVERITY[DECLARED.get(id)] } : {}),
|
|
54
63
|
};
|
|
64
|
+
const taxon = cweTaxon(report.ruleCwe?.[id]);
|
|
55
65
|
return {
|
|
56
66
|
id,
|
|
57
67
|
shortDescription: { text: report.ruleText?.[id] || id },
|
|
@@ -59,10 +69,23 @@ const descriptorFor = (report) => (id) => {
|
|
|
59
69
|
// rule. `help` is where GitHub code scanning shows a recommendation.
|
|
60
70
|
...(report.ruleImpact?.[id] ? { fullDescription: { text: textOf(report.ruleImpact[id]) } } : {}),
|
|
61
71
|
...(report.ruleRemedy?.[id] ? { help: { text: textOf(report.ruleRemedy[id]) } } : {}),
|
|
72
|
+
...(taxon ? { relationships: [{ target: { id: taxon, toolComponent: CWE_COMPONENT } }] } : {}),
|
|
62
73
|
...(Object.keys(properties).length ? { properties } : {}),
|
|
63
74
|
};
|
|
64
75
|
};
|
|
65
76
|
|
|
77
|
+
function cweTaxonomy(report, ruleIds) {
|
|
78
|
+
const taxa = [...new Set(ruleIds.map((id) => cweTaxon(report.ruleCwe?.[id])).filter(Boolean))].sort((a, b) => a - b);
|
|
79
|
+
if (!taxa.length) return [];
|
|
80
|
+
return [{
|
|
81
|
+
name: CWE_COMPONENT.name,
|
|
82
|
+
organization: 'MITRE',
|
|
83
|
+
shortDescription: { text: 'Common Weakness Enumeration' },
|
|
84
|
+
informationUri: 'https://cwe.mitre.org/',
|
|
85
|
+
taxa: taxa.map((id) => ({ id, helpUri: `https://cwe.mitre.org/data/definitions/${id}.html` })),
|
|
86
|
+
}];
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
function notifications(report) {
|
|
67
90
|
const bySet = report.summary?.bySet || {};
|
|
68
91
|
const execution = [];
|
|
@@ -111,6 +134,9 @@ function notifications(report) {
|
|
|
111
134
|
properties: { set },
|
|
112
135
|
});
|
|
113
136
|
}
|
|
137
|
+
for (const [id, warnings] of [['cobolwork/site-key-ignored', report.summary?.siteWarnings], ['cobolwork/feed-key-ignored', report.summary?.feedWarnings]]) {
|
|
138
|
+
for (const w of warnings || []) configuration.push({ descriptor: { id }, level: 'warning', message: { text: textOf(w) } });
|
|
139
|
+
}
|
|
114
140
|
// A diff whose two sides were scanned under different configuration.
|
|
115
141
|
for (const { file, change } of report.summary?.configurationChanged || []) {
|
|
116
142
|
configuration.push({
|
|
@@ -144,6 +170,16 @@ export function toSarif(report, { toolVersion = '0.1.0' } = {}) {
|
|
|
144
170
|
|
|
145
171
|
const { execution, configuration } = notifications(report);
|
|
146
172
|
const s = report.summary || {};
|
|
173
|
+
const taxonomies = cweTaxonomy(report, present);
|
|
174
|
+
|
|
175
|
+
// Every file a result or related location names, each listed once; locations point at it by index.
|
|
176
|
+
const results = [...report.findings, ...suppressed];
|
|
177
|
+
const uris = [...new Set(results.flatMap((f) => [f.path, ...located(f).map((r) => r.path)]).map(uriOf))].sort();
|
|
178
|
+
const artifactIndex = new Map(uris.map((uri, i) => [uri, i]));
|
|
179
|
+
const artifactLocation = (path) => {
|
|
180
|
+
const uri = uriOf(path);
|
|
181
|
+
return { uri, uriBaseId: '%SRCROOT%', index: artifactIndex.get(uri) };
|
|
182
|
+
};
|
|
147
183
|
|
|
148
184
|
return {
|
|
149
185
|
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
@@ -158,6 +194,7 @@ export function toSarif(report, { toolVersion = '0.1.0' } = {}) {
|
|
|
158
194
|
},
|
|
159
195
|
...(extensions.length ? { extensions } : {}),
|
|
160
196
|
},
|
|
197
|
+
...(taxonomies.length ? { taxonomies } : {}),
|
|
161
198
|
invocations: [{
|
|
162
199
|
// The verdict, where a consumer that reads nothing else will still see it.
|
|
163
200
|
executionSuccessful: s.nosrc !== true,
|
|
@@ -183,7 +220,8 @@ export function toSarif(report, { toolVersion = '0.1.0' } = {}) {
|
|
|
183
220
|
},
|
|
184
221
|
},
|
|
185
222
|
}],
|
|
186
|
-
|
|
223
|
+
...(uris.length ? { artifacts: uris.map((uri) => ({ location: { uri, uriBaseId: '%SRCROOT%' } })) } : {}),
|
|
224
|
+
results: results.map((f) => {
|
|
187
225
|
const set = SET_OF.get(f.rule);
|
|
188
226
|
const component = set !== undefined ? indexOfSet.get(set) : undefined;
|
|
189
227
|
const within = component === undefined ? null : extensions[component].rules.findIndex((r) => r.id === f.rule);
|
|
@@ -213,9 +251,12 @@ export function toSarif(report, { toolVersion = '0.1.0' } = {}) {
|
|
|
213
251
|
...(f.reach ? { reach: f.reach } : {}),
|
|
214
252
|
...(f.effect ? { effect: f.effect } : {}),
|
|
215
253
|
...(f.exploitability ? { exploitability: f.exploitability } : {}),
|
|
254
|
+
// How an abend's input reached the program: through its own entry, or as arguments to
|
|
255
|
+
// a subprogram no caller run is shown to pass.
|
|
256
|
+
...(f.abend?.inputFrom ? { inputFrom: f.abend.inputFrom } : {}),
|
|
216
257
|
} } : {}),
|
|
217
|
-
locations: [{ physicalLocation: { artifactLocation:
|
|
218
|
-
...(f.
|
|
258
|
+
locations: [{ physicalLocation: { artifactLocation: artifactLocation(f.path), region: { startLine: f.line || 1 } } }],
|
|
259
|
+
...(located(f).length ? { relatedLocations: located(f).map((r) => ({ physicalLocation: { artifactLocation: artifactLocation(r.path), region: { startLine: r.line || 1 } }, message: { text: textOf(r.detail) } })) } : {}),
|
|
219
260
|
...(f.suppressed ? { suppressions: [{ kind: 'external', status: 'accepted', justification: `${f.suppressed.action} by ${f.suppressed.who} until ${f.suppressed.expires}: ${f.suppressed.reason}` }] } : {}),
|
|
220
261
|
};
|
|
221
262
|
}),
|
package/lib/scan.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { join, resolve } from 'node:path';
|
|
|
3
3
|
import { sortFindings, tally, evidenceMap, EVIDENCE, EXPLOITABILITY } from './kernel/findings.mjs';
|
|
4
4
|
import { directoryTree } from './kernel/source-tree.mjs';
|
|
5
5
|
import { REGISTRY, RULE_SETS, ALL_RULES, reportKey } from './kernel/registry.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { inventorySteps } from './inventory.mjs';
|
|
7
|
+
import { driveTogether, sharingTree } from './kernel/shared-pass.mjs';
|
|
7
8
|
import { stoppedBecause } from './kernel/memory.mjs';
|
|
8
9
|
import { complianceMap, FRAMEWORKS_LOADED, FRAMEWORKS_UNLOADED } from './compliance.mjs';
|
|
9
10
|
import { stampFingerprints, FINGERPRINT_VERSION } from './kernel/identity.mjs';
|
|
@@ -65,6 +66,9 @@ function mergeEstate(reports) {
|
|
|
65
66
|
effectNote: effectless.length ? `no authority facts declared in ${of(effectless)}, so no finding there was judged to run privileged: name privilegedTransactions/privilegedJobs in each repository's cobolwork.site.json, or bring them in the COBOLWORK_REACH extract` : null,
|
|
66
67
|
witnessFeeds: first('witnessFeeds'), byWitness: addUp('byWitness'), witnessFeedProblems: union('witnessFeedProblems'),
|
|
67
68
|
witnessUnmatched: unmatched, witnessOverruled: overruled,
|
|
69
|
+
executionFeeds: first('executionFeeds'), byExecution: addUp('byExecution'), executionFeedProblems: union('executionFeedProblems'),
|
|
70
|
+
siteWarnings: all.flatMap((x) => (x.s.siteWarnings || []).map((w) => `${x.repo}/${w}`)),
|
|
71
|
+
feedWarnings: union('feedWarnings'),
|
|
68
72
|
};
|
|
69
73
|
return Object.fromEntries(Object.entries(merged).filter(([, v]) => (Array.isArray(v) ? v.length : v)));
|
|
70
74
|
}
|
|
@@ -101,7 +105,7 @@ function scanEach(root, opts) {
|
|
|
101
105
|
filesScanned, programs: sum('programs'), programFiles: sum('programFiles'), copybookFiles: sum('copybookFiles'),
|
|
102
106
|
jclFiles: sum('jclFiles'), copiesMissing: sum('copiesMissing'), filesUnreadable: sum('filesUnreadable'),
|
|
103
107
|
filesEbcdic: sum('filesEbcdic'), filesOverBudget: sum('filesOverBudget'), crossProgram: sum('crossProgram'),
|
|
104
|
-
ruleSets: opts.only || RULE_SETS, nosrc: filesScanned === 0,
|
|
108
|
+
ruleSets: opts.only || RULE_SETS, flowModel: FLOW_MODEL, toolVersion: TOOL_VERSION, nosrc: filesScanned === 0,
|
|
105
109
|
coverageIncomplete: reports.some(({ r }) => r.summary.coverageIncomplete),
|
|
106
110
|
identity: { version: FINGERPRINT_VERSION, shared },
|
|
107
111
|
...(reports[0]?.r.summary.advisoryFeeds ? { advisoryFeeds: reports[0].r.summary.advisoryFeeds } : {}),
|
|
@@ -157,7 +161,10 @@ export function applyEstateFacts(findings, checked, root, opts = {}) {
|
|
|
157
161
|
const { byExecution } = applyExecution(findings, root, executionFeeds);
|
|
158
162
|
const driven = byExploitability ? byExploitability['attacker-driven'] : 0;
|
|
159
163
|
const loaded = witnessFeeds.filter((f) => !f.problem);
|
|
164
|
+
const feedWarnings = [...reachFeeds, ...witnessFeeds].flatMap((f) => f.warnings).sort();
|
|
160
165
|
return {
|
|
166
|
+
...(site.warnings.length ? { siteWarnings: site.warnings } : {}),
|
|
167
|
+
...(feedWarnings.length ? { feedWarnings } : {}),
|
|
161
168
|
...(byReach ? { byReach } : {}),
|
|
162
169
|
...(byEffect ? { byEffect } : {}),
|
|
163
170
|
...(reachFeeds.some(f => !f.problem) ? { reachFeeds: reachFeeds.filter(f => !f.problem).map(f => ({ file: f.file, extract: f.extract, retrieved: f.retrieved })) } : {}),
|
|
@@ -178,13 +185,12 @@ export function applyEstateFacts(findings, checked, root, opts = {}) {
|
|
|
178
185
|
}
|
|
179
186
|
|
|
180
187
|
// A set that throws is reported as not having run, and the other sets' findings stand.
|
|
188
|
+
const notRun = (set, e) => report(set.name, { rules: set.rules, findings: [], stats: {
|
|
189
|
+
coverageIncomplete: true,
|
|
190
|
+
notRead: `the rule set stopped on an error and reported nothing: ${printable(e?.message ?? e)}`,
|
|
191
|
+
} });
|
|
181
192
|
function runSet(set, root, o) {
|
|
182
|
-
try { return set.scan(root, o); } catch (e) {
|
|
183
|
-
return report(set.name, { rules: set.rules, findings: [], stats: {
|
|
184
|
-
coverageIncomplete: true,
|
|
185
|
-
notRead: `the rule set stopped on an error and reported nothing: ${printable(e?.message ?? e)}`,
|
|
186
|
-
} });
|
|
187
|
-
}
|
|
193
|
+
try { return set.scan(root, o); } catch (e) { return notRun(set, e); }
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
export function scanAll(root, opts = {}) {
|
|
@@ -193,12 +199,22 @@ export function scanAll(root, opts = {}) {
|
|
|
193
199
|
// One walk of the tree for the whole scan. Every rule set used to call buildFileIndex(root) for
|
|
194
200
|
// itself, so a scan read every directory entry and resolved every symlink nine times over, and
|
|
195
201
|
// added a tenth walk each time a rule set was added.
|
|
196
|
-
const tree = opts.tree || directoryTree(root, opts);
|
|
202
|
+
const tree = sharingTree(opts.tree || directoryTree(root, opts));
|
|
197
203
|
const o = { ...opts, tree };
|
|
198
|
-
const inv = inventory(root, o);
|
|
199
204
|
// One loop over the registry, in its order. A set that is not registered cannot run, and
|
|
200
|
-
// cannot report a silent zero on its own behalf either.
|
|
201
|
-
|
|
205
|
+
// cannot report a silent zero on its own behalf either. The inventory and the sets that read
|
|
206
|
+
// programs share one pass, so each program is read and parsed once.
|
|
207
|
+
const chosen = REGISTRY.filter((set) => only.includes(set.name));
|
|
208
|
+
const stepped = chosen.filter((set) => set.steps);
|
|
209
|
+
const [invResult, ...stepResults] = driveTogether([inventorySteps(root, o), ...stepped.map((set) => set.steps(root, o))], tree);
|
|
210
|
+
if (invResult && invResult.error) throw invResult.error;
|
|
211
|
+
const inv = invResult;
|
|
212
|
+
const byName = new Map(stepped.map((set, k) => [set.name, stepResults[k]]));
|
|
213
|
+
const parts = chosen.map((set) => {
|
|
214
|
+
if (!byName.has(set.name)) return runSet(set, root, o);
|
|
215
|
+
const r = byName.get(set.name);
|
|
216
|
+
return r && r.error ? notRun(set, r.error) : r;
|
|
217
|
+
});
|
|
202
218
|
|
|
203
219
|
const flowPart = parts.find(p => p.tool === 'cobolwork-flow');
|
|
204
220
|
const overBudget = flowPart ? flowPart.summary.filesOverBudget || 0 : 0;
|
package/lib/sets/abend.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { createHash } from 'node:crypto';
|
|
|
10
10
|
import { delimiter, join, posix, basename } from 'node:path';
|
|
11
11
|
import { report } from '../kernel/ruleset.mjs';
|
|
12
12
|
import { verifyEvidence } from '../evidence/verify.mjs';
|
|
13
|
+
import { NOT_RUN_ABENDS } from '../ironwork-ids.mjs';
|
|
13
14
|
|
|
14
15
|
export const ABEND_RULES = {
|
|
15
16
|
'input-causes-abend-s0c7': {
|
|
@@ -30,6 +31,18 @@ export const ABEND_RULES = {
|
|
|
30
31
|
impact: 'SSRANGE caught the overrun and ended the run; compiled without SSRANGE, as production builds often are, the same input reads or writes storage beside the table or field',
|
|
31
32
|
remedy: 'Check the index against both ends of the table or field before the statement the journal names, and keep SSRANGE on for the build',
|
|
32
33
|
},
|
|
34
|
+
'input-causes-hang': {
|
|
35
|
+
sev: 'med', evidence: 'execution', cwe: 'CWE-834',
|
|
36
|
+
text: 'An input the program accepts keeps a loop running past the statement limit that stands in for its time limit (S322)',
|
|
37
|
+
impact: 'Whoever supplies that input decides how long the loop runs; on z/OS a long enough run holds the step and its processor until TIME= ends it, and the jobs behind it wait. The run shows the loop passed the limit, not that it would never end',
|
|
38
|
+
remedy: 'Find the loop at the line the journal names and bound its iterations: check the count or the exit condition the input supplies before the loop starts, or stop it at a counter and reject the input that would pass it',
|
|
39
|
+
},
|
|
40
|
+
'input-selects-program': {
|
|
41
|
+
sev: 'high', evidence: 'execution', cwe: 'CWE-470',
|
|
42
|
+
text: 'An input the program accepts chooses the program a CALL loads',
|
|
43
|
+
impact: "Whoever supplies that input picks which load module runs with this program's authority and data; the fuzz run named one the library does not hold, so the run ended in S806, but a name the library does hold runs",
|
|
44
|
+
remedy: 'Choose the called program from a fixed set of names checked against the input, or call it by a literal name, at the CALL the journal names',
|
|
45
|
+
},
|
|
33
46
|
'input-causes-abend': {
|
|
34
47
|
sev: 'med', evidence: 'execution', cwe: 'CWE-248',
|
|
35
48
|
text: 'An input the program accepts ends its run with an abend',
|
|
@@ -38,19 +51,44 @@ export const ABEND_RULES = {
|
|
|
38
51
|
},
|
|
39
52
|
};
|
|
40
53
|
|
|
54
|
+
// What the manifest's `optimized` says of an abend: whether the same input ends the same way with
|
|
55
|
+
// the program compiled at OPTIMIZE(2), where IBM may compare invalid data by its bytes rather than
|
|
56
|
+
// end in a data exception at OPTIMIZE(0), its default.
|
|
57
|
+
const OPTIMIZED = {
|
|
58
|
+
true: '; compiled with OPTIMIZE(2) it ends the same way',
|
|
59
|
+
false: '; it holds at OPTIMIZE(0), IBM\'s default: compiled with OPTIMIZE(2) the same input does not end there',
|
|
60
|
+
};
|
|
61
|
+
|
|
41
62
|
// IBM's messages for an SSRANGE failure: a subscript or index, an OCCURS DEPENDING ON object, and a
|
|
42
63
|
// reference modification's start, length, and start plus length.
|
|
43
64
|
const RANGE_MESSAGES = /^IGZ00(06|07|72|73|74)S\b/;
|
|
44
65
|
|
|
66
|
+
// Language Environment's message for a dynamic CALL of a module no library holds, which ironwork
|
|
67
|
+
// ends U4038 (its assumption C450); earlier ironwork releases ended the same CALL S806.
|
|
68
|
+
const MODULE_NOT_FOUND = /^CEE3501S The module .* was not found\.$/;
|
|
69
|
+
|
|
45
70
|
export function abendRunPaths(opts = {}) {
|
|
46
71
|
if (opts.abendRuns) return opts.abendRuns;
|
|
47
72
|
return (process.env.COBOLWORK_ABENDS || '').split(delimiter).filter(Boolean);
|
|
48
73
|
}
|
|
49
74
|
|
|
75
|
+
// How ironwork ends the message of a data or protection exception in a CICS task, whose abend
|
|
76
|
+
// code CICS reports as ASRA.
|
|
77
|
+
const CICS_PROGRAM_CHECK = /\((S0C7|S0C4), which CICS reports as ASRA\)$/;
|
|
78
|
+
|
|
79
|
+
// How ironwork ends the message of a Language Environment condition in a CICS task, U4038 in
|
|
80
|
+
// batch, which CICS reports as transaction abend 4038 (its assumption C454).
|
|
81
|
+
const CICS_LE_CONDITION = / \(U4038, which CICS reports as transaction abend 4038\)$/;
|
|
82
|
+
|
|
50
83
|
export function abendRule(abend) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
84
|
+
const cicsLe = abend.code === '4038' && CICS_LE_CONDITION.test(abend.message || '');
|
|
85
|
+
const message = cicsLe ? abend.message.replace(CICS_LE_CONDITION, '') : abend.message || '';
|
|
86
|
+
const code = cicsLe ? 'U4038' : (abend.code === 'ASRA' && CICS_PROGRAM_CHECK.exec(message)?.[1]) || abend.code;
|
|
87
|
+
if (code === 'S0C7') return 'input-causes-abend-s0c7';
|
|
88
|
+
if (code === 'S0C4') return 'input-causes-abend-s0c4';
|
|
89
|
+
if (code === 'S322') return 'input-causes-hang';
|
|
90
|
+
if (code === 'S806' || (code === 'U4038' && MODULE_NOT_FOUND.test(message))) return 'input-selects-program';
|
|
91
|
+
if (RANGE_MESSAGES.test(message)) return 'input-causes-abend-subscript-range';
|
|
54
92
|
return 'input-causes-abend';
|
|
55
93
|
}
|
|
56
94
|
|
|
@@ -63,16 +101,35 @@ function journalAbend(evidenceDir, runId) {
|
|
|
63
101
|
let abend = null;
|
|
64
102
|
const inputs = [];
|
|
65
103
|
const calls = [];
|
|
104
|
+
const sinks = [];
|
|
66
105
|
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
67
106
|
if (!line) continue;
|
|
68
107
|
const rec = JSON.parse(line);
|
|
69
108
|
if (rec.kind === 'abend') abend ??= rec;
|
|
70
109
|
else if (rec.kind === 'input') inputs.push(rec);
|
|
71
110
|
else if (rec.kind === 'call') calls.push(rec);
|
|
111
|
+
else if (rec.kind === 'sink') sinks.push(rec);
|
|
72
112
|
}
|
|
73
|
-
return abend && { ...abend, inputs, calls };
|
|
113
|
+
return abend && { ...abend, inputs, calls, sinks };
|
|
74
114
|
}
|
|
75
115
|
|
|
116
|
+
const MANIFEST_FORMAT = 'ironwork-fuzz/v1';
|
|
117
|
+
// A subprogram fuzzed at its interface: its inputs are arguments, and a caller passing them is
|
|
118
|
+
// not shown to exist (ironwork docs/evidence.md §5.2).
|
|
119
|
+
const INTERFACE_FORMAT = 'ironwork-fuzz-interface/v1';
|
|
120
|
+
const FORMATS = [MANIFEST_FORMAT, INTERFACE_FORMAT];
|
|
121
|
+
|
|
122
|
+
// What a finding from an interface run says of the caller its input needs.
|
|
123
|
+
function interfaceClause(callers) {
|
|
124
|
+
const where = callers.length
|
|
125
|
+
? `with arguments shaped by the CALL${callers.length === 1 ? '' : 's'} at ${callers.map((c) => `${c.file}:${c.line}`).join(', ')}`
|
|
126
|
+
: 'on generated arguments, with no CALL to it in the scanned tree';
|
|
127
|
+
return `; it ran as a subprogram, ${where}, on its first call in its initial state: a caller passing these bytes ends it, and no caller run shows one does`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ironwork's own abend codes for a construct it does not run: no IBM system would raise them.
|
|
131
|
+
const NOT_MODELLED = new Set(NOT_RUN_ABENDS);
|
|
132
|
+
|
|
76
133
|
const isString = (x) => typeof x === 'string' && x.length > 0;
|
|
77
134
|
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
78
135
|
|
|
@@ -113,6 +170,15 @@ export function loadAbendRun(dir, tree) {
|
|
|
113
170
|
out.problems.push('manifest.json is not an ironwork-fuzz manifest with a program, inputs and runs');
|
|
114
171
|
return out;
|
|
115
172
|
}
|
|
173
|
+
// A manifest from before ironwork wrote `format` has the first format's shape.
|
|
174
|
+
if (doc.format !== undefined && !FORMATS.includes(doc.format)) {
|
|
175
|
+
out.problems.push(`manifest.json is in format ${JSON.stringify(doc.format)}, and this cobolwork reads ${FORMATS.join(' and ')}`);
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
const inputFrom = doc.format === INTERFACE_FORMAT ? 'interface' : 'entry';
|
|
179
|
+
const callers = inputFrom === 'interface' && Array.isArray(doc.callers)
|
|
180
|
+
? doc.callers.filter((c) => c && isString(c.file) && Number.isInteger(c.line)).map((c) => ({ file: posix.normalize(c.file.replace(/\\/g, '/')), line: c.line }))
|
|
181
|
+
: [];
|
|
116
182
|
const programFile = posix.normalize(doc.program.file.replace(/\\/g, '/'));
|
|
117
183
|
if (programFile.startsWith('../') || posix.isAbsolute(programFile)) {
|
|
118
184
|
out.problems.push(`the program ${doc.program.file} is outside the scanned tree`);
|
|
@@ -131,7 +197,7 @@ export function loadAbendRun(dir, tree) {
|
|
|
131
197
|
const seen = new Map();
|
|
132
198
|
for (const run of doc.runs) {
|
|
133
199
|
if (!run || run.outcome !== 'abend' || !run.abend || !isString(run.abend.code)) continue;
|
|
134
|
-
if (run.abend.code
|
|
200
|
+
if (NOT_MODELLED.has(run.abend.code)) { out.notModelled++; continue; }
|
|
135
201
|
const recorded = journalAbend(evidenceDir, run.journal);
|
|
136
202
|
if (!recorded || recorded.code !== run.abend.code) {
|
|
137
203
|
out.problems.push(`run ${run.journal ?? '(unnamed)'}: its journal does not record the abend ${run.abend.code} the manifest gives`);
|
|
@@ -152,16 +218,23 @@ export function loadAbendRun(dir, tree) {
|
|
|
152
218
|
const where = isString(place.file) ? posix.normalize(posix.join(rootDir ?? posix.dirname(programFile), place.file.replace(/\\/g, '/'))) : programFile;
|
|
153
219
|
const line = Number.isInteger(place.line) && place.line > 0 ? place.line : 1;
|
|
154
220
|
const rule = abendRule(run.abend);
|
|
221
|
+
// A CALL of a missing program is input choosing the program only where the run's own journal
|
|
222
|
+
// records the marker fuzz put in the input reaching that CALL's program load.
|
|
223
|
+
if (rule === 'input-selects-program' && !recorded.sinks.some((k) => k.sink === 'dynamic-program-load' && k.reached === true && k.file === place.file && k.line === place.line)) {
|
|
224
|
+
out.problems.push(`run ${run.journal}: its journal does not record the input reaching the CALL at ${place.file}:${place.line}, so its missing-program ending is not reported`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
155
227
|
const used = (Array.isArray(run.input) ? run.input : []).map((id) => inputs.get(id)).filter(Boolean)
|
|
156
|
-
.map((i) => ({ kind: i.kind, name: i.name, bytes: i.bytes, ...(i.minimized ? { minimized: true } : {}) }));
|
|
228
|
+
.map((i) => ({ kind: i.kind, name: i.name, bytes: i.bytes, ...(i.minimized ? { minimized: true } : {}), ...(Number.isInteger(i.position) ? { position: i.position } : {}), ...(i.omitted === true ? { omitted: true } : {}) }));
|
|
157
229
|
const key = `${rule}|${where}|${line}`;
|
|
158
230
|
const size = used.reduce((n, i) => n + String(i.bytes || '').length, 0);
|
|
159
231
|
const held = seen.get(key);
|
|
160
232
|
if (held && held.size <= size) continue;
|
|
233
|
+
const optimized = typeof run.abend.optimized === 'boolean' ? run.abend.optimized : undefined;
|
|
161
234
|
const finding = {
|
|
162
235
|
rule, path: where, line, program: out.program.id,
|
|
163
|
-
detail: `${out.program.id || programFile} ended with ${run.abend.code}${isString(run.abend.message) ? ` (${run.abend.message})` : ''} on an input the fuzz run ${out.dir} kept`,
|
|
164
|
-
abend: { code: run.abend.code, ...(isString(run.abend.message) ? { message: run.abend.message } : {}) },
|
|
236
|
+
detail: `${out.program.id || programFile} ended with ${run.abend.code}${isString(run.abend.message) ? ` (${run.abend.message})` : ''} on an input the fuzz run ${out.dir} kept${OPTIMIZED[optimized] ?? ''}${inputFrom === 'interface' ? interfaceClause(callers) : ''}`,
|
|
237
|
+
abend: { code: run.abend.code, ...(isString(run.abend.message) ? { message: run.abend.message } : {}), ...(optimized === undefined ? {} : { optimized }), inputFrom, ...(inputFrom === 'interface' ? { callers } : {}) },
|
|
165
238
|
input: used, run: { dir: out.dir, journal: run.journal },
|
|
166
239
|
};
|
|
167
240
|
seen.set(key, { size, finding });
|
|
@@ -179,6 +252,7 @@ export function scanAbend(root, opts = {}) {
|
|
|
179
252
|
abendRuns: runs.map((r) => ({ dir: r.dir, program: r.program ? r.program.file : null, findings: r.findings.length, ...(r.notModelled ? { notModelled: r.notModelled } : {}), ...(r.counts ? { counts: r.counts } : {}) })),
|
|
180
253
|
...(runs.some((r) => r.problems.length) ? { abendRunProblems: runs.flatMap((r) => r.problems.map((p) => `${r.dir}: ${p}`)) } : {}),
|
|
181
254
|
...(runs.some((r) => r.findings.some((f) => !existsSync(join(root, f.path)))) ? { abendRunsElsewhere: runs.flatMap((r) => r.findings.filter((f) => !existsSync(join(root, f.path))).map((f) => `${r.dir}: ${f.path}`)) } : {}),
|
|
255
|
+
byInputFrom: { entry: findings.filter((f) => f.abend.inputFrom === 'entry').length, interface: findings.filter((f) => f.abend.inputFrom === 'interface').length },
|
|
182
256
|
setIncomplete: runs.some((r) => r.problems.length > 0),
|
|
183
257
|
};
|
|
184
258
|
return report('abend', { rules: ABEND_RULES, findings, stats, run: null });
|
package/lib/sets/build.mjs
CHANGED
|
@@ -55,7 +55,7 @@ const COMPILERS = new Set(['gnucobol', 'opentext-cobol', 'ibm-enterprise-cobol']
|
|
|
55
55
|
|
|
56
56
|
// Files that decide what the build runs. A repository states this in one of a small number of
|
|
57
57
|
// conventional places, and reading only those keeps the rule from matching prose in a README.
|
|
58
|
-
function isBuildFile(path) {
|
|
58
|
+
export function isBuildFile(path) {
|
|
59
59
|
const name = basename(path);
|
|
60
60
|
const p = path.replace(/\\/g, '/').toLowerCase();
|
|
61
61
|
if (/^(dockerfile|containerfile)(\..+)?$/i.test(name)) return true;
|
|
@@ -98,6 +98,21 @@ const PINS = [
|
|
|
98
98
|
{ product: 'cics-transaction-gateway', re: /from\s+\S*cics[-_]?transaction[-_]?gateway\S*:v?(\d+(?:\.\d+)+)/gi },
|
|
99
99
|
];
|
|
100
100
|
|
|
101
|
+
// Every product version the text pins, once each, with the text that pins it.
|
|
102
|
+
export function pinsIn(src) {
|
|
103
|
+
const out = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const { product, re } of PINS) {
|
|
106
|
+
for (const m of src.matchAll(re)) {
|
|
107
|
+
const key = `${product}@${m[1]}`;
|
|
108
|
+
if (seen.has(key)) continue;
|
|
109
|
+
seen.add(key);
|
|
110
|
+
out.push({ product, version: m[1], text: m[0] });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
function withMavenProperties(src) {
|
|
102
117
|
const block = src.match(/<properties>([\s\S]*?)<\/properties>/i);
|
|
103
118
|
if (!block) return src;
|
|
@@ -189,42 +204,33 @@ export function scanBuild(root, opts = {}) {
|
|
|
189
204
|
if (/pom\.xml$/i.test(path)) src = withMavenProperties(src);
|
|
190
205
|
const lines = src.split(/\r?\n/);
|
|
191
206
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
});
|
|
220
|
-
} else {
|
|
221
|
-
findings.push({
|
|
222
|
-
rule: `build-pins-vulnerable-${kind}`, path, line,
|
|
223
|
-
detail: `${path} pins ${product} ${version}, which ${countOf(hits)}: ${ids.join(', ')}${fromFeeds(hits)}` +
|
|
224
|
-
(age.stale ? `. The KEV snapshot is ${age.days} days old, so whether any of these is now known to be exploited was not checked against a current catalogue` : ''),
|
|
225
|
-
related: hits.map(relatedOf),
|
|
226
|
-
});
|
|
227
|
-
}
|
|
207
|
+
for (const { product, version, text } of pinsIn(src)) {
|
|
208
|
+
stats.pinsFound++;
|
|
209
|
+
|
|
210
|
+
const hits = advisoriesFor(product, version, extra);
|
|
211
|
+
if (!hits.length) continue;
|
|
212
|
+
|
|
213
|
+
// A pin written across lines, as Maven writes one, is reported at its version.
|
|
214
|
+
const needle = text.includes('\n') ? text.split('\n').pop().trim() : text;
|
|
215
|
+
const line = lines.findIndex((l) => l.includes(needle)) + 1 || 1;
|
|
216
|
+
const exploited = hits.filter((a) => isKnownExploited(a.id));
|
|
217
|
+
const ids = hits.map((a) => a.id).sort(byText);
|
|
218
|
+
|
|
219
|
+
const kind = COMPILERS.has(product) ? 'compiler' : 'component';
|
|
220
|
+
if (exploited.length) {
|
|
221
|
+
findings.push({
|
|
222
|
+
rule: `build-pins-exploited-${kind}`, path, line,
|
|
223
|
+
detail: `${path} pins ${product} ${version}, which ${exploited.map((a) => a.id).sort(byText).join(', ')} affects and CISA lists as known to be exploited${fromFeeds(hits)}`,
|
|
224
|
+
related: hits.map(relatedOf),
|
|
225
|
+
knownExploited: exploited.map((a) => a.id).sort(byText),
|
|
226
|
+
});
|
|
227
|
+
} else {
|
|
228
|
+
findings.push({
|
|
229
|
+
rule: `build-pins-vulnerable-${kind}`, path, line,
|
|
230
|
+
detail: `${path} pins ${product} ${version}, which ${countOf(hits)}: ${ids.join(', ')}${fromFeeds(hits)}` +
|
|
231
|
+
(age.stale ? `. The KEV snapshot is ${age.days} days old, so whether any of these is now known to be exploited was not checked against a current catalogue` : ''),
|
|
232
|
+
related: hits.map(relatedOf),
|
|
233
|
+
});
|
|
228
234
|
}
|
|
229
235
|
}
|
|
230
236
|
return src.length;
|
package/lib/sets/cics.mjs
CHANGED
|
@@ -4,9 +4,10 @@ import { report } from '../kernel/ruleset.mjs';
|
|
|
4
4
|
import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { parseSource } from '../parser.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { drive, loopOver } from '../kernel/shared-pass.mjs';
|
|
8
8
|
import { buildControl, hasFact } from '../control.mjs';
|
|
9
9
|
import { parseCsd } from '../csd.mjs';
|
|
10
|
+
import { execReading } from '../exec-reading.mjs';
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
// `impact` says who can do what once the construct is present, so a reader can tell a defect they
|
|
@@ -113,27 +114,6 @@ export function signOnTest(test) {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
|
|
116
|
-
function execOptions(exec) {
|
|
117
|
-
const opts = new Map();
|
|
118
|
-
const words = [];
|
|
119
|
-
const toks = exec.toks;
|
|
120
|
-
for (let i = 0; i < toks.length; i++) {
|
|
121
|
-
const t = toks[i];
|
|
122
|
-
if (t.t !== 'word') continue;
|
|
123
|
-
words.push(t.u);
|
|
124
|
-
if (toks[i + 1] && toks[i + 1].t === 'sep' && toks[i + 1].v === '(') {
|
|
125
|
-
const inner = [];
|
|
126
|
-
let depth = 1;
|
|
127
|
-
for (let k = i + 2; k < toks.length && depth > 0; k++) {
|
|
128
|
-
if (toks[k].t === 'sep') { depth += toks[k].v === '(' ? 1 : -1; if (depth === 0) break; continue; }
|
|
129
|
-
inner.push(toks[k]);
|
|
130
|
-
}
|
|
131
|
-
if (!opts.has(t.u)) opts.set(t.u, inner);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return { opts, words };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
117
|
const subtreeRead = (x) => x.directRefs > 0 || x.children.some(subtreeRead);
|
|
138
118
|
|
|
139
119
|
// Whether EIBCALEN bounds what the program reads: a condition tests it, or a field it was moved or
|
|
@@ -164,8 +144,8 @@ function testsLength(p) {
|
|
|
164
144
|
// Commands whose failure a program ordinarily lets pass: removing a temporary-storage queue that may
|
|
165
145
|
// not exist, a line written to a transient-data log, the clock, a screen sent to a terminal that may
|
|
166
146
|
// have gone, an ASSIGN, and the RETURN that ends the task anyway.
|
|
167
|
-
const QUIET_FAILURES = (
|
|
168
|
-
|| ['ASKTIME', 'FORMATTIME', 'SEND', 'ASSIGN', 'RETURN'].includes(
|
|
147
|
+
const QUIET_FAILURES = ({ verb, sub }) => (verb === 'DELETEQ' && sub === 'TS') || (verb === 'WRITEQ' && sub === 'TD')
|
|
148
|
+
|| ['ASKTIME', 'FORMATTIME', 'SEND', 'ASSIGN', 'RETURN'].includes(verb);
|
|
169
149
|
const SHOWN_IGNORED = 5;
|
|
170
150
|
|
|
171
151
|
// Commands whose outcome the program cannot see: NOHANDLE with no RESP and no EIBRESP test before the
|
|
@@ -175,15 +155,16 @@ function conditionsIgnored(p, path) {
|
|
|
175
155
|
const execs = p.execs.filter((e) => e.kind === 'CICS').sort((a, b) => a.line - b.line);
|
|
176
156
|
const hits = [];
|
|
177
157
|
execs.forEach((e, k) => {
|
|
178
|
-
const
|
|
179
|
-
|
|
158
|
+
const reading = execReading(e);
|
|
159
|
+
const { opts: o, words, verb, sub } = reading;
|
|
160
|
+
if (verb === 'IGNORE' && sub === 'CONDITION') {
|
|
180
161
|
hits.push({ e, what: `IGNORE CONDITION ${words.slice(2).join(' ')}`.trim() });
|
|
181
162
|
return;
|
|
182
163
|
}
|
|
183
|
-
if (!words.includes('NOHANDLE') || o.has('RESP') || QUIET_FAILURES(
|
|
164
|
+
if (!words.includes('NOHANDLE') || o.has('RESP') || QUIET_FAILURES(reading)) return;
|
|
184
165
|
const until = execs[k + 1] ? execs[k + 1].line : Infinity;
|
|
185
166
|
const tested = p.statements.some((st) => st.line > e.line && st.line < until && (st.sources || []).some((t) => t.t === 'word' && t.u === 'EIBRESP'));
|
|
186
|
-
if (!tested) hits.push({ e, what: `${
|
|
167
|
+
if (!tested) hits.push({ e, what: `${verb}${sub && sub !== 'NOHANDLE' && !o.has(sub) ? ` ${sub}` : ''} NOHANDLE` });
|
|
187
168
|
});
|
|
188
169
|
if (!hits.length) return null;
|
|
189
170
|
const shown = hits.slice(0, SHOWN_IGNORED).map((h) => `${h.what} at line ${h.e.line}`).join(', ');
|
|
@@ -201,7 +182,7 @@ function conditionsIgnored(p, path) {
|
|
|
201
182
|
// away from, and this set kept doing it: on a 100,000-program repository it exhausted an 8 GB heap
|
|
202
183
|
// and took a corpus run with it. What survives per program is an identifier, a communication area's
|
|
203
184
|
// size, one boolean, and a short list of transfers.
|
|
204
|
-
export function
|
|
185
|
+
export function* scanCicsSteps(root, opts = {}) {
|
|
205
186
|
const tree = treeFor(root, opts);
|
|
206
187
|
const files = tree.list().filter(isProgram).filter(inScope(opts));
|
|
207
188
|
// Identifier to the size of that program's DFHCOMMAREA, which is all a caller needs of a callee.
|
|
@@ -226,8 +207,7 @@ export function scanCics(root, opts = {}) {
|
|
|
226
207
|
const transfers = [];
|
|
227
208
|
for (const e of p.execs) {
|
|
228
209
|
if (e.kind !== 'CICS') continue;
|
|
229
|
-
const { opts: o,
|
|
230
|
-
const verb = w[0];
|
|
210
|
+
const { opts: o, verb } = execReading(e);
|
|
231
211
|
if (!['LINK', 'XCTL', 'START'].includes(verb)) continue;
|
|
232
212
|
const pgm = (o.get('PROGRAM') || o.get('TRANSID') || [])[0];
|
|
233
213
|
const areaTok = (o.get('COMMAREA') || o.get('FROM') || [])[0];
|
|
@@ -275,7 +255,7 @@ export function scanCics(root, opts = {}) {
|
|
|
275
255
|
// asking the security manager, and one that folds a password's case first. Passwords are recognised
|
|
276
256
|
// by field name, as the sign-on rules above recognise them, and each finding says so.
|
|
277
257
|
function passwordChecks(p, path) {
|
|
278
|
-
if (p.execs.some((e) => e.kind === 'CICS' && /^(VERIFY|SIGNON)$/.test(
|
|
258
|
+
if (p.execs.some((e) => e.kind === 'CICS' && /^(VERIFY|SIGNON)$/.test(execReading(e).verb || ''))) return [];
|
|
279
259
|
const byName = new Map();
|
|
280
260
|
for (const it of p.items) if (!byName.has(it.name)) byName.set(it.name, it);
|
|
281
261
|
const resolve = (tok) => (tok && tok.t === 'word' ? p.resolved.get(tok) || byName.get(tok.u) || null : null);
|
|
@@ -286,8 +266,8 @@ export function scanCics(root, opts = {}) {
|
|
|
286
266
|
// FETCH fills, and everything under any of them. A copy made by MOVE first is not followed.
|
|
287
267
|
const read = new Set();
|
|
288
268
|
for (const e of p.execs) {
|
|
289
|
-
const { opts: o,
|
|
290
|
-
if (e.kind === 'CICS' && /^READ(NEXT|PREV)?$/.test(
|
|
269
|
+
const { opts: o, verb } = execReading(e);
|
|
270
|
+
if (e.kind === 'CICS' && /^READ(NEXT|PREV)?$/.test(verb || '')) for (const t of o.get('INTO') || []) if (t.t === 'word') read.add(t.u);
|
|
291
271
|
if (e.kind === 'SQL' && (e.hostVariables || []).length) for (const h of e.hostVariables) if (h.written && h.tok) read.add(h.tok.u);
|
|
292
272
|
}
|
|
293
273
|
const stored = (name) => {
|
|
@@ -343,15 +323,15 @@ export function scanCics(root, opts = {}) {
|
|
|
343
323
|
const sends = [];
|
|
344
324
|
for (const e of p.execs) {
|
|
345
325
|
if (e.kind !== 'CICS') continue;
|
|
346
|
-
const { opts: o,
|
|
347
|
-
const opt =
|
|
326
|
+
const { opts: o, verb } = execReading(e);
|
|
327
|
+
const opt = verb === 'XCTL' || verb === 'LINK' ? 'PROGRAM' : (verb === 'RETURN' || verb === 'START') ? 'TRANSID' : null;
|
|
348
328
|
const t = opt && (o.get(opt) || [])[0];
|
|
349
329
|
if (!t || t.t !== 'lit') continue;
|
|
350
330
|
const target = `${opt === 'PROGRAM' ? '' : 'transaction '}${String(t.v).trim().toUpperCase()}`;
|
|
351
331
|
if (target === p.id) continue;
|
|
352
332
|
const bits = ctl.facts.get(ctl.nodeOf.get(e));
|
|
353
333
|
if (!bits) continue;
|
|
354
|
-
sends.push({ e, verb
|
|
334
|
+
sends.push({ e, verb, target, signedOn: hasFact(bits, bit) });
|
|
355
335
|
}
|
|
356
336
|
const granted = new Set(sends.filter((x) => x.signedOn).map((x) => x.target));
|
|
357
337
|
const aid = p.refs.some((r) => r.tok.u === 'EIBAID');
|
|
@@ -364,7 +344,7 @@ export function scanCics(root, opts = {}) {
|
|
|
364
344
|
// Releasing each tree bounds what one program costs; it does not bound what a hundred thousand of
|
|
365
345
|
// them cost together. The shared loop watches the heap and stops before V8 cannot collect, and
|
|
366
346
|
// names every file it did not reach.
|
|
367
|
-
const run =
|
|
347
|
+
const run = yield loopOver(files, (f) => {
|
|
368
348
|
let src;
|
|
369
349
|
try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
|
|
370
350
|
if (!/EXEC\s+CICS/i.test(src) && !/(?:^|[\s.])COPY\s+/i.test(src)) return src.length;
|
|
@@ -445,3 +425,5 @@ export function scanCics(root, opts = {}) {
|
|
|
445
425
|
|
|
446
426
|
return report('cics', { rules: CICS_RULES, findings, stats, run });
|
|
447
427
|
}
|
|
428
|
+
|
|
429
|
+
export const scanCics = (root, opts = {}) => drive(scanCicsSteps(root, opts));
|