@portll/cobolwork 0.2.75 → 0.2.117
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 +69 -15
- package/bin/cobolwork.mjs +126 -9
- package/lib/arith.mjs +235 -0
- package/lib/baseline.mjs +5 -0
- package/lib/build.mjs +112 -30
- package/lib/capabilities.mjs +12 -7
- package/lib/consequence.mjs +29 -0
- package/lib/control.mjs +1052 -91
- package/lib/csd.mjs +17 -2
- package/lib/dataflow.mjs +245 -78
- package/lib/enterprise-options.mjs +255 -0
- package/lib/equivalence.mjs +124 -0
- package/lib/evidence/cli.mjs +65 -0
- package/lib/evidence/journal.mjs +85 -0
- package/lib/evidence/record.mjs +110 -0
- package/lib/evidence/run.mjs +111 -0
- package/lib/evidence/seal.mjs +177 -0
- package/lib/evidence/slsa.mjs +64 -0
- package/lib/evidence/sshsig.mjs +172 -0
- package/lib/evidence/store.mjs +170 -0
- package/lib/evidence/verify.mjs +167 -0
- package/lib/explain.mjs +4 -1
- package/lib/exploitability.mjs +263 -0
- package/lib/ironwork.mjs +111 -0
- package/lib/kernel/findings.mjs +11 -0
- package/lib/kernel/registry.mjs +4 -0
- package/lib/option-diff.mjs +49 -0
- package/lib/options.mjs +109 -46
- package/lib/parser.mjs +29 -7
- package/lib/policy.mjs +8 -3
- package/lib/precompile.mjs +4 -0
- package/lib/reach.mjs +29 -12
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +1 -0
- package/lib/sbom.mjs +147 -0
- package/lib/scan.mjs +93 -27
- package/lib/sets/cics.mjs +114 -0
- package/lib/sets/flow.mjs +82 -2
- package/lib/sets/jcl.mjs +67 -0
- package/lib/sets/semantics.mjs +377 -0
- package/lib/sets/web.mjs +34 -10
- package/lib/sets/zowe.mjs +220 -0
- package/lib/sources.mjs +10 -0
- package/lib/tui/app.mjs +31 -13
- package/lib/tui/model.mjs +17 -2
- package/lib/utilities.mjs +85 -13
- package/lib/verify.mjs +128 -0
- package/package.json +1 -1
- package/rules/compliance-dora.json +507 -0
- package/rules/compliance-ffiec.json +507 -0
- package/rules/compliance-nist80053.json +507 -0
- package/schema/cobolwork.policy.schema.json +2 -1
package/README.md
CHANGED
|
@@ -23,8 +23,10 @@ COBOL, exits zero and reports a clean result.
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
25
25
|
npm install -g @portll/cobolwork
|
|
26
|
+
pip install cobolwork
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
Either installs the command `cobolwork`; the PyPI package runs the same files with the Node.js on
|
|
29
|
+
your PATH. The same package is attached to each
|
|
28
30
|
[release](https://github.com/Portll/cobolwork/releases) as `cobolwork-<version>.tgz`, which is the
|
|
29
31
|
one to pin, and as `cobolwork.tgz`, which
|
|
30
32
|
`npm install -g https://github.com/Portll/cobolwork/releases/latest/download/cobolwork.tgz`
|
|
@@ -34,7 +36,8 @@ follows. To run from a checkout instead:
|
|
|
34
36
|
|
|
35
37
|
Node 18 or later. No dependencies, runtime or development. `npm link` puts `cobolwork` on your PATH;
|
|
36
38
|
adding `bin/` to PATH does the same thing. GnuCOBOL is needed only to regrade the parser or validate
|
|
37
|
-
benchmark cases.
|
|
39
|
+
benchmark cases. In a GitHub workflow, `uses: Portll/cobolwork@main` runs the build gate on a pull
|
|
40
|
+
request and writes SARIF for code scanning: [docs/github-action.md](docs/github-action.md).
|
|
38
41
|
|
|
39
42
|
## What it does
|
|
40
43
|
|
|
@@ -42,7 +45,7 @@ benchmark cases.
|
|
|
42
45
|
|---|---|
|
|
43
46
|
| `cobolwork scan <path>` | every rule set below, as JSON or SARIF |
|
|
44
47
|
| `cobolwork inventory <path>` | inventory (and what couldn't be read) |
|
|
45
|
-
| `cobolwork flow <path>` | where untrusted data reaches a sensitive operation, with the path it took |
|
|
48
|
+
| `cobolwork flow <path>` | where untrusted data reaches a sensitive operation, with the path it took and whether an attacker can use it |
|
|
46
49
|
| `cobolwork diff <repo> --base <ref>` | what a change reaches: layouts it moves in programs nobody edited, new call targets, findings it adds or removes |
|
|
47
50
|
| `cobolwork build <repo> [--base <ref>] [-- <compiler> …]` | the build gate: every finding ranked LOW to KNOWN-EXPLOITABLE, the build stopped on the ones the policy blocks and on compiler options that let a bad index corrupt storage, and the compiler run only on a pass |
|
|
48
51
|
| `cobolwork parse <file>` | one file's structure, for debugging |
|
|
@@ -126,12 +129,63 @@ decides who acts on it. Every rule declares one of eight finding types:
|
|
|
126
129
|
`coverage` and `context` are exactly the `info` rules (not defects). A consumer that counts these
|
|
127
130
|
findings should leave them out of its counts.
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
`path` is a route found by reading the code, not by running it. Its precision has been measured on
|
|
133
|
+
benchmark cases this project wrote, not yet on an independently labelled corpus.
|
|
134
|
+
|
|
135
|
+
### Can an attacker use it?
|
|
136
|
+
|
|
137
|
+
Every `path` finding carries a verdict, the facts it rests on, and the one fact that would change it:
|
|
138
|
+
|
|
139
|
+
| `exploitability` | What it means | What to do |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `confirmed` | the estate's own test reproduced it, and brought the result as a witness feed | patch now |
|
|
142
|
+
| `exploitable` | input an attacker supplies reaches the operation with no check on every route, and the estate declares a transaction or job that carries it open to any user | patch first |
|
|
143
|
+
| `attacker-driven` | the same route, but nobody has declared who may start its entries | patch, or declare the entries |
|
|
144
|
+
| `restricted` | the same route, behind a control the estate named on every entry | patch; the control is not a check |
|
|
145
|
+
| `mitigated` | a check runs first but is not shown to stop the value, or SSRANGE turns an overrun into an abend | confirm the check |
|
|
146
|
+
| `upstream` | the value is a file record or database row, so whoever can write it drives it | review who writes the data |
|
|
147
|
+
| `refuted` | a check leaves only safe values, or no route reaches the operation | nothing |
|
|
148
|
+
|
|
149
|
+
For the terminal route to dynamic SQL in `test/fixtures/entry`, scanned with a site file that
|
|
150
|
+
declares its transaction open:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
"exploitability": {
|
|
154
|
+
"verdict": "exploitable",
|
|
155
|
+
"drivenBy": "A terminal user",
|
|
156
|
+
"because": [
|
|
157
|
+
"A terminal user supplies it: EXEC CICS RECEIVE at INQUIRY.cbl:10",
|
|
158
|
+
"no check is shown to run on every route before the operation at INQUIRY.cbl:13",
|
|
159
|
+
"started by transaction INQ1: open to any user, per cobolwork.site.json"
|
|
160
|
+
],
|
|
161
|
+
"unknown": "whether it reproduces: a test on a system the estate owns, under its own authorisation, is the only confirmation",
|
|
162
|
+
"fixAt": {
|
|
163
|
+
"program": "INQUIRY", "path": "INQUIRY.cbl", "line": 13, "item": "WS-STMT",
|
|
164
|
+
"test": "before the operation at INQUIRY.cbl:13, on every route to it, test WS-STMT against a list of the values allowed (...), and let only a value that passes reach it; a static statement with host variables needs no test"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`fixAt` is where one patch covers every route, with a test the analysis credits: patch it there, scan
|
|
170
|
+
again, and the finding moves to `refuted`. Where no test makes the operation safe, `why` says so and
|
|
171
|
+
the rule's remedy is the fix.
|
|
172
|
+
|
|
173
|
+
Who may start a transaction lives in RACF, not in a repository, so without it the strongest verdict is
|
|
174
|
+
`attacker-driven`, and `summary.reachNote` says how many that is. Name the entries in
|
|
175
|
+
`cobolwork.site.json` (`openTransactions`, `restrictedTransactions`, `openJobs`, `restrictedJobs`), or
|
|
176
|
+
bring a reduced RACF unload as `COBOLWORK_REACH`, which is refused from inside the scanned tree.
|
|
177
|
+
`node diag/propose-site.mjs <path>` lists the transactions and jobs to ask about, most findings
|
|
178
|
+
first, with the program each runs and any listener or URI map that puts it on the network; it
|
|
179
|
+
proposes none of them as open or restricted.
|
|
180
|
+
|
|
181
|
+
The tool never confirms a finding from reading: that needs a test on the running system. The estate
|
|
182
|
+
brings its own results as `COBOLWORK_WITNESS`, keyed by fingerprint with the outcome, who ran the
|
|
183
|
+
test, when and on which system, and refused from inside the tree like `COBOLWORK_REACH`. A reproduced
|
|
184
|
+
finding becomes `confirmed`; one the estate's test did not reproduce keeps its verdict and says so,
|
|
185
|
+
since a test that missed the route does not show it safe.
|
|
186
|
+
`summary.byExploitability` counts the verdicts, and a report holding an `exploitable` finding carries
|
|
187
|
+
`summary.handling`, because it is then a list of what to attack first. Upload it only where the RACF
|
|
188
|
+
facts behind it may go. [docs/spec/reach.md](docs/spec/reach.md) §9 is the full specification.
|
|
135
189
|
|
|
136
190
|
### What each finding lets someone do, and the fix
|
|
137
191
|
|
|
@@ -189,7 +243,7 @@ A scan says what each rule set read, not only what it found:
|
|
|
189
243
|
"why": "no cobolwork.site.json: the production-name and production-dataset rules did not run, because nothing declares what production means in this estate" }
|
|
190
244
|
],
|
|
191
245
|
"flowModel": "byte-range",
|
|
192
|
-
"toolVersion": "0.2.
|
|
246
|
+
"toolVersion": "0.2.90"
|
|
193
247
|
}
|
|
194
248
|
```
|
|
195
249
|
|
|
@@ -234,7 +288,7 @@ gate. An incomplete scan is undecided, never a pass.
|
|
|
234
288
|
|
|
235
289
|
The compiler options are held to the policy too. A program compiled without `SSRANGE`, or with
|
|
236
290
|
`SSRANGE(MSG)`, which reports a bad subscript and carries on, fails; for `cobc` the missing `-fec`
|
|
237
|
-
checks are added to the command
|
|
291
|
+
checks are added to the command, and for GCC's `gcobol` the missing `-fcobol-exceptions`. `cobolwork.policy.json` changes any of this, and `--policy <file>`
|
|
238
292
|
names an organisation's floor, which a repository can tighten and never loosen.
|
|
239
293
|
[docs/spec/build-gate.md](docs/spec/build-gate.md) is the full specification.
|
|
240
294
|
|
|
@@ -259,7 +313,7 @@ stand-in in `diag/precompiler.mjs`, which rewrites what the parser would otherwi
|
|
|
259
313
|
tests compare the parser with the compiler's answers kept in `test/fixtures/parser/*.golden.json`,
|
|
260
314
|
so they run without GnuCOBOL.
|
|
261
315
|
|
|
262
|
-
`bench/cases/` holds
|
|
316
|
+
`bench/cases/` holds 102 CWE-labelled cases, each paired with a near-miss negative: the same shape
|
|
263
317
|
with the flaw removed. `node bench/run.mjs` scores any scanner's findings against them, by rule and
|
|
264
318
|
file, never by line, and `npm test` fails if any case scores differently from its declaration.
|
|
265
319
|
`--validate` compiles every COBOL case with GnuCOBOL and checks every JCL case against the
|
|
@@ -289,9 +343,9 @@ Every rule is mapped to the clause of each framework that makes it an obligation
|
|
|
289
343
|
|
|
290
344
|
| File | Instrument | Rules mapped |
|
|
291
345
|
|---|---|---|
|
|
292
|
-
| `rules/compliance-dora.json` | Regulation (EU) 2022/2554 (DORA) |
|
|
293
|
-
| `rules/compliance-ffiec.json` | FFIEC IT Examination Handbook |
|
|
294
|
-
| `rules/compliance-nist80053.json` | NIST SP 800-53 Rev. 5.2.0 |
|
|
346
|
+
| `rules/compliance-dora.json` | Regulation (EU) 2022/2554 (DORA) | 203 |
|
|
347
|
+
| `rules/compliance-ffiec.json` | FFIEC IT Examination Handbook | 201, and 2 recorded as unmapped |
|
|
348
|
+
| `rules/compliance-nist80053.json` | NIST SP 800-53 Rev. 5.2.0 | 201, and 2 recorded as unmapped |
|
|
295
349
|
|
|
296
350
|
`node diag/map-compliance.mjs` refuses to write a quote the cached instrument does not contain, and
|
|
297
351
|
every scan carries the mapping as `ruleCompliance`. The clause choice is a judgement that no
|
package/bin/cobolwork.mjs
CHANGED
|
@@ -4,9 +4,10 @@ import { writeFileSync, readFileSync, lstatSync, statSync } from 'node:fs';
|
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { readdirSync } from 'node:fs';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { scanAll, RULE_SETS } from '../lib/scan.mjs';
|
|
7
|
+
import { scanAll, applyEstateFacts, RULE_SETS } from '../lib/scan.mjs';
|
|
8
8
|
import { scan as scanFlow } from '../lib/sets/flow.mjs';
|
|
9
9
|
import { inventory } from '../lib/inventory.mjs';
|
|
10
|
+
import { sbom } from '../lib/sbom.mjs';
|
|
10
11
|
import { parseFile } from '../lib/parser.mjs';
|
|
11
12
|
import { toSarif } from '../lib/sarif.mjs';
|
|
12
13
|
import { diffRefs } from '../lib/diff.mjs';
|
|
@@ -15,12 +16,19 @@ import { build, buildSarif, buildSummaryLine } from '../lib/build.mjs';
|
|
|
15
16
|
import { capabilities } from '../lib/capabilities.mjs';
|
|
16
17
|
import { commitAt, revisionOf, toolRevision } from '../lib/revision.mjs';
|
|
17
18
|
import { stampFingerprints } from '../lib/kernel/identity.mjs';
|
|
19
|
+
import { tally } from '../lib/kernel/findings.mjs';
|
|
18
20
|
import { loadBaseline, applyBaseline, baselineEntries, BASELINE_FILE, SUPPRESSING } from '../lib/baseline.mjs';
|
|
19
21
|
import { readReport } from '../lib/tui/model.mjs';
|
|
20
22
|
import { nodeTerminal } from '../lib/tui/terminal.mjs';
|
|
21
23
|
import { runTui } from '../lib/tui/run.mjs';
|
|
22
24
|
import { explainFinding } from '../lib/explain.mjs';
|
|
23
25
|
import { printable } from '../lib/kernel/printable.mjs';
|
|
26
|
+
import { startEvidence, recordInputs, recordHashed, recordFindings, recordOutput, recordVerdict, recordBaselineWrite, finishEvidence } from '../lib/evidence/run.mjs';
|
|
27
|
+
import { evidenceCommand } from '../lib/evidence/cli.mjs';
|
|
28
|
+
import { slsaStatement } from '../lib/evidence/slsa.mjs';
|
|
29
|
+
import { parseAllowedSigners } from '../lib/evidence/sshsig.mjs';
|
|
30
|
+
|
|
31
|
+
const STARTED = new Date().toISOString();
|
|
24
32
|
|
|
25
33
|
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
26
34
|
|
|
@@ -28,6 +36,9 @@ const USAGE = `cobolwork ${VERSION} — COBOL, JCL and CICS security analysis, n
|
|
|
28
36
|
|
|
29
37
|
cobolwork scan <path> every rule set: data flow, CICS, JCL, hidden content, copybooks, build pins
|
|
30
38
|
cobolwork inventory <path> what is there and what could not be read (copybooks, dialects)
|
|
39
|
+
cobolwork sbom <path> [--name <estate>]
|
|
40
|
+
a CycloneDX 1.6 bill of materials: every source by SHA-256, what each
|
|
41
|
+
program copies and calls, what each job runs, and the platforms used
|
|
31
42
|
cobolwork flow <path> data-flow findings with the path the data took, as JSON
|
|
32
43
|
cobolwork parse <file> parse one source file and print a summary
|
|
33
44
|
cobolwork diff <repo> --base <ref> [--head <ref>]
|
|
@@ -47,11 +58,17 @@ const USAGE = `cobolwork ${VERSION} — COBOL, JCL and CICS security analysis, n
|
|
|
47
58
|
what this cobolwork can do: commands and options, the version of every
|
|
48
59
|
document it writes, its fingerprint version, the file kinds it reads, and
|
|
49
60
|
the commit it runs from
|
|
50
|
-
cobolwork build <repo> [--base <ref>] [--policy <file>] [--provenance <file>] [-- <compiler> <arg>...]
|
|
61
|
+
cobolwork build <repo> [--base <ref>] [--policy <file>] [--provenance <file>] [--ironwork <path> | -- <compiler> <arg>...]
|
|
51
62
|
the build gate: every finding ranked LOW to KNOWN-EXPLOITABLE, the
|
|
52
63
|
policy's blocking findings and compiler options checked, and the
|
|
53
64
|
compiler run only on a pass. Exits 0 pass, 1 fail, 3 undecided, 4 the
|
|
54
65
|
compiler failed after a pass, 2 could not run
|
|
66
|
+
cobolwork evidence verify|seal|anchor --evidence <dir>
|
|
67
|
+
the evidence a run records: verify its chains, seals and witnesses;
|
|
68
|
+
seal the ledger (--ssh-key <file> or --signer <program>); anchor a seal
|
|
69
|
+
in a git witness (--anchor-git <repo> [--push]) or write an RFC 3161
|
|
70
|
+
request (--tsq <file>). verify exits 0 sealed, 1 broken or contradicted,
|
|
71
|
+
3 undetermined
|
|
55
72
|
|
|
56
73
|
Options
|
|
57
74
|
--format json|sarif output format (default json)
|
|
@@ -81,6 +98,22 @@ Options
|
|
|
81
98
|
--policy <file> build: an organisation's floor policy, from outside the repository; the
|
|
82
99
|
repository's cobolwork.policy.json can tighten it and never loosen it
|
|
83
100
|
--provenance <file> build: write what was scanned, under which policy, and what was compiled
|
|
101
|
+
--provenance-format cobolwork|slsa build: the record as written today, or an in-toto statement with
|
|
102
|
+
the SLSA Provenance v1 predicate (unsigned; the pipeline signs it)
|
|
103
|
+
--artifact <path>[,<path>] build: what the compiler produced, named as subjects of the SLSA statement
|
|
104
|
+
--equivalence <file>[,<file>] build --base: ironwork equivalence statements for the programs the
|
|
105
|
+
change edits; with --allowed-signers each must be signed by one. The policy's
|
|
106
|
+
requireEquivalence (never, machineAuthored, always) says when one is required
|
|
107
|
+
--ironwork <path> build: after a pass, run ironwork check on every program, for an estate that
|
|
108
|
+
compiles with IBM Enterprise COBOL; a program ironwork rejects exits 4, one it
|
|
109
|
+
does not model yet leaves the build undecided
|
|
110
|
+
--evidence <dir> scan, flow, diff, gate, build, baseline, inventory: record this run in a
|
|
111
|
+
hash-chained journal and ledger there (or COBOLWORK_EVIDENCE); never inside
|
|
112
|
+
the tree being read
|
|
113
|
+
--allowed-signers <file> evidence verify: OpenSSH allowed_signers for namespace cobolwork-evidence
|
|
114
|
+
--anchor-git <repo>, --ref <ref>, --push, --max-unsealed <n>
|
|
115
|
+
evidence: the git witness, the ref that counts (default @{upstream}), whether
|
|
116
|
+
anchor pushes, and how many ledger records may follow the newest seal
|
|
84
117
|
|
|
85
118
|
Exit codes: 0 the command ran, 2 it could not run. A run that examined nothing says so in
|
|
86
119
|
summary.filesScanned and summary.nosrc rather than reporting a clean zero.
|
|
@@ -127,6 +160,21 @@ function parseArgs(argv) {
|
|
|
127
160
|
else if (a === '--json') opts.json = true;
|
|
128
161
|
else if (a === '--policy') opts.policy = value();
|
|
129
162
|
else if (a === '--provenance') opts.provenance = value();
|
|
163
|
+
else if (a === '--provenance-format') opts.provenanceFormat = value();
|
|
164
|
+
else if (a === '--equivalence') opts.equivalence = [...(opts.equivalence || []), ...list().map((x) => resolve(x))];
|
|
165
|
+
else if (a === '--artifact') opts.artifact = [...(opts.artifact || []), ...list().map((x) => resolve(x))];
|
|
166
|
+
else if (a === '--ironwork') opts.ironwork = value();
|
|
167
|
+
else if (a === '--evidence') opts.evidence = value();
|
|
168
|
+
else if (a === '--name') opts.name = value();
|
|
169
|
+
else if (a === '--ssh-key') opts.sshKey = value();
|
|
170
|
+
else if (a === '--signer') opts.signer = value();
|
|
171
|
+
else if (a === '--allowed-signers') opts.allowedSigners = value();
|
|
172
|
+
else if (a === '--anchor-git') opts.anchorGit = value();
|
|
173
|
+
else if (a === '--ref') opts.ref = value();
|
|
174
|
+
else if (a === '--push') opts.push = true;
|
|
175
|
+
else if (a === '--max-unsealed') opts.maxUnsealed = value();
|
|
176
|
+
else if (a === '--tsq') opts.tsq = value();
|
|
177
|
+
else if (a === '--expect-key') opts.expectKey = value();
|
|
130
178
|
else if (a === '--help' || a === '-h') opts.help = true;
|
|
131
179
|
else if (a === '--version' || a === '-v') opts.version = true;
|
|
132
180
|
else if (a.startsWith('-')) { opts.unknown = a; }
|
|
@@ -151,8 +199,11 @@ function warnCoverage(report) {
|
|
|
151
199
|
|
|
152
200
|
const isLink = (p) => { try { return lstatSync(p).isSymbolicLink(); } catch { return false; } };
|
|
153
201
|
|
|
202
|
+
let journal = null;
|
|
203
|
+
|
|
154
204
|
function emit(obj, opts) {
|
|
155
205
|
const text = JSON.stringify(obj, null, opts.quiet ? 0 : 1);
|
|
206
|
+
recordOutput(journal, 'report', text + '\n', opts.out || null);
|
|
156
207
|
if (opts.out) { writeFileSync(opts.out, text + '\n'); return; }
|
|
157
208
|
// A pipe drains asynchronously and process.exit discards what has not drained: past 64 KB the
|
|
158
209
|
// reader got a truncated document and exit 0. The process ends on its own once stdout is flushed.
|
|
@@ -202,7 +253,7 @@ if (gateFlag && opts._.length && opts._[0] !== 'gate') {
|
|
|
202
253
|
process.stderr.write(`cobolwork: --${gateFlag.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)} is for gate only\n`);
|
|
203
254
|
process.exit(2);
|
|
204
255
|
}
|
|
205
|
-
const COPYLIB_COMMANDS = ['scan', 'flow', 'inventory', 'diff', 'build', 'baseline', 'tui', 'explain'];
|
|
256
|
+
const COPYLIB_COMMANDS = ['scan', 'flow', 'inventory', 'diff', 'build', 'baseline', 'tui', 'explain', 'sbom'];
|
|
206
257
|
if (opts.copylib && opts._.length && !COPYLIB_COMMANDS.includes(opts._[0])) {
|
|
207
258
|
process.stderr.write(`cobolwork: ${opts._[0]} reads no copybooks; ${COPYLIB_COMMANDS.join(', ')} do\n`);
|
|
208
259
|
process.exit(2);
|
|
@@ -215,7 +266,9 @@ if (opts.json && opts._.length && opts._[0] !== 'capabilities') {
|
|
|
215
266
|
process.stderr.write(`cobolwork: --json is for capabilities; every other command writes JSON unless --format says otherwise\n`);
|
|
216
267
|
process.exit(2);
|
|
217
268
|
}
|
|
218
|
-
const buildFlag = ['policy', 'provenance'].find((k) => opts[k] !== undefined) || (compilerArgv ? '' : null);
|
|
269
|
+
const buildFlag = ['policy', 'provenance', 'provenanceFormat', 'artifact', 'equivalence', 'ironwork'].find((k) => opts[k] !== undefined) || (compilerArgv ? '' : null);
|
|
270
|
+
if (opts.provenanceFormat !== undefined && !['cobolwork', 'slsa'].includes(opts.provenanceFormat)) { process.stderr.write(`cobolwork: --provenance-format takes cobolwork or slsa; got ${opts.provenanceFormat}\n`); process.exit(2); }
|
|
271
|
+
if ((opts.provenanceFormat !== undefined || opts.artifact) && !opts.provenance) { process.stderr.write('cobolwork: --provenance-format and --artifact describe the --provenance file; name it\n'); process.exit(2); }
|
|
219
272
|
if (buildFlag !== null && opts._.length && opts._[0] !== 'build') {
|
|
220
273
|
process.stderr.write(`cobolwork: ${buildFlag ? `--${buildFlag}` : '--'} is for build only\n`);
|
|
221
274
|
process.exit(2);
|
|
@@ -223,11 +276,32 @@ if (buildFlag !== null && opts._.length && opts._[0] !== 'build') {
|
|
|
223
276
|
// A misspelled set would run nothing and report a clean zero.
|
|
224
277
|
const badSets = (opts.only || []).filter(s => !RULE_SETS.includes(s));
|
|
225
278
|
if (badSets.length || (opts.only && !opts.only.length)) { process.stderr.write(`cobolwork: --only takes ${RULE_SETS.join(',')}; got ${badSets.join(',') || 'nothing'}\n`); process.exit(2); }
|
|
279
|
+
const JOURNALED = ['scan', 'flow', 'diff', 'gate', 'build', 'baseline', 'inventory', 'sbom'];
|
|
280
|
+
if (opts.name !== undefined && opts._.length && opts._[0] !== 'sbom') { process.stderr.write('cobolwork: --name is for sbom only\n'); process.exit(2); }
|
|
281
|
+
if (opts.evidence !== undefined && opts._.length && ![...JOURNALED, 'evidence'].includes(opts._[0])) {
|
|
282
|
+
process.stderr.write(`cobolwork: ${opts._[0]} records no evidence; ${JOURNALED.join(', ')} and evidence do\n`);
|
|
283
|
+
process.exit(2);
|
|
284
|
+
}
|
|
285
|
+
const evidenceFlag = ['sshKey', 'signer', 'allowedSigners', 'anchorGit', 'ref', 'push', 'maxUnsealed', 'tsq', 'expectKey'].find((k) => opts[k] !== undefined);
|
|
286
|
+
if (opts.equivalence && !opts.base) { process.stderr.write('cobolwork: --equivalence judges a change; it needs --base\n'); process.exit(2); }
|
|
287
|
+
if (evidenceFlag && opts._.length && opts._[0] !== 'evidence' && !(evidenceFlag === 'allowedSigners' && opts._[0] === 'build')) {
|
|
288
|
+
process.stderr.write(`cobolwork: --${evidenceFlag.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)} is for evidence only\n`);
|
|
289
|
+
process.exit(2);
|
|
290
|
+
}
|
|
226
291
|
|
|
227
292
|
const [command, target] = opts._;
|
|
228
293
|
const root = resolve(target || '.');
|
|
229
294
|
const repos = opts.repos ? readdirSync(root, { withFileTypes: true }).filter(d => d.isDirectory() && !d.name.startsWith('.')).map(d => d.name) : [''];
|
|
230
295
|
|
|
296
|
+
if (JOURNALED.includes(command)) {
|
|
297
|
+
try {
|
|
298
|
+
journal = startEvidence(opts, { command, argv, roots: [root], toolVersion: VERSION });
|
|
299
|
+
} catch (e) {
|
|
300
|
+
process.stderr.write(`cobolwork: ${printable(e && e.message ? e.message : e)}\n`);
|
|
301
|
+
process.exit(2);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
231
305
|
function storedReport(file) {
|
|
232
306
|
const path = resolve(file);
|
|
233
307
|
let doc;
|
|
@@ -254,12 +328,22 @@ function baselinedScan() {
|
|
|
254
328
|
try {
|
|
255
329
|
if (command === 'capabilities') {
|
|
256
330
|
emit(capabilities(), opts);
|
|
331
|
+
} else if (command === 'evidence') {
|
|
332
|
+
process.exitCode = evidenceCommand(target, opts, { toolVersion: VERSION, write: (s) => process.stdout.write(s) });
|
|
257
333
|
} else if (command === 'scan' || command === 'flow') {
|
|
258
334
|
const flowOpts = { repos, fullTrace: opts.fullTrace === true, systemDirs };
|
|
259
335
|
const report = command === 'flow' ? scanFlow(root, flowOpts) : scanAll(root, { ...flowOpts, only: opts.only, advisoryFeeds: opts.advisoryFeeds });
|
|
260
|
-
if (command === 'flow')
|
|
336
|
+
if (command === 'flow') {
|
|
337
|
+
report.summary.identity = stampFingerprints(report.findings, { root });
|
|
338
|
+
stampFingerprints(report.checked, { root });
|
|
339
|
+
Object.assign(report.summary, applyEstateFacts(report.findings, report.checked, root));
|
|
340
|
+
// A route the estate reproduced moves from checked back to findings.
|
|
341
|
+
Object.assign(report.summary, { findings: report.findings.length, byRule: tally(report.findings), checked: report.checked.length });
|
|
342
|
+
}
|
|
261
343
|
applyBaseline(report, loadBaseline(root, { explicit: opts.baseline ? resolve(opts.baseline) : null, use: !opts.noBaseline }));
|
|
262
344
|
stampRevisions(report.summary);
|
|
345
|
+
recordInputs(journal, 0, root);
|
|
346
|
+
recordFindings(journal, report);
|
|
263
347
|
if (command === 'flow') emit(report, opts);
|
|
264
348
|
else if (opts.format === 'sarif') emit(toSarif(report, { toolVersion: VERSION }), opts);
|
|
265
349
|
else emit(opts.quiet ? { tool: report.tool, schemaVersion: report.schemaVersion, summary: report.summary } : report, opts);
|
|
@@ -268,6 +352,7 @@ try {
|
|
|
268
352
|
if (!opts.base) { process.stderr.write(`cobolwork: diff needs --base <ref>\n`); process.exit(2); }
|
|
269
353
|
const report = diffRefs(root, opts.base, opts.head || null, { only: opts.only, fullTrace: opts.fullTrace === true, systemDirs });
|
|
270
354
|
stampRevisions(report.summary, opts.head || null);
|
|
355
|
+
recordFindings(journal, { findings: [...report.findings, ...(report.introduced || [])] });
|
|
271
356
|
if (opts.format === 'sarif') emit(toSarif({ ...report, findings: [...report.findings, ...report.introduced] }, { toolVersion: VERSION }), opts);
|
|
272
357
|
else emit(opts.quiet ? { tool: report.tool, schemaVersion: report.schemaVersion, summary: report.summary } : report, opts);
|
|
273
358
|
} else if (command === 'gate') {
|
|
@@ -280,13 +365,14 @@ try {
|
|
|
280
365
|
stampRevisions(doc.summary, opts.head || null);
|
|
281
366
|
emit(doc, opts);
|
|
282
367
|
if (opts.exitCode) process.exitCode = VERDICT_EXIT[doc.verdict];
|
|
368
|
+
recordVerdict(journal, doc, process.exitCode ?? 0);
|
|
283
369
|
} else if (command === 'build') {
|
|
284
370
|
// A rule set left out is findings not seen, and a waiver file from elsewhere is not the one the
|
|
285
371
|
// change was reviewed against.
|
|
286
372
|
if (opts.only || opts.repos) { process.stderr.write('cobolwork: build judges one repository with every rule set; --only and --repos do not apply\n'); process.exit(2); }
|
|
287
373
|
if (opts.baseline) { process.stderr.write('cobolwork: build reads the baseline the change was written against; --baseline does not apply, --no-baseline does\n'); process.exit(2); }
|
|
288
374
|
if (opts.head && !opts.base) { process.stderr.write('cobolwork: build --head needs --base\n'); process.exit(2); }
|
|
289
|
-
const result = build(root, { base: opts.base || null, head: opts.head || null, policy: opts.policy || null, noBaseline: opts.noBaseline === true, compiler: compilerArgv, advisoryFeeds: opts.advisoryFeeds || null, copylibs: systemDirs });
|
|
375
|
+
const result = build(root, { base: opts.base || null, head: opts.head || null, policy: opts.policy || null, noBaseline: opts.noBaseline === true, compiler: compilerArgv, ironwork: opts.ironwork || null, advisoryFeeds: opts.advisoryFeeds || null, copylibs: systemDirs, equivalence: opts.equivalence || [], allowed: opts.allowedSigners ? parseAllowedSigners(readFileSync(resolve(opts.allowedSigners), 'utf8')) : null });
|
|
290
376
|
stampRevisions(result.doc.summary, opts.head || null);
|
|
291
377
|
Object.assign(result.report.summary, { toolRevision: result.doc.summary.toolRevision, revision: result.doc.summary.revision });
|
|
292
378
|
Object.assign(result.provenance, { toolRevision: result.doc.summary.toolRevision, revision: result.doc.summary.revision });
|
|
@@ -294,9 +380,25 @@ try {
|
|
|
294
380
|
const report = result.report;
|
|
295
381
|
emit(buildSarif(toSarif({ ...report, findings: report.findings }, { toolVersion: VERSION }), result.doc), opts);
|
|
296
382
|
} else emit(result.doc, opts);
|
|
297
|
-
|
|
383
|
+
recordHashed(journal, 0, result.provenance.sources);
|
|
384
|
+
recordFindings(journal, result.report);
|
|
385
|
+
if (opts.provenance) {
|
|
386
|
+
const record = opts.provenanceFormat === 'slsa'
|
|
387
|
+
? slsaStatement({
|
|
388
|
+
provenance: result.provenance, root, artifacts: opts.artifact || [],
|
|
389
|
+
docBytes: Buffer.from(JSON.stringify(result.doc, null, opts.quiet ? 0 : 1) + '\n'),
|
|
390
|
+
runId: journal ? journal.id : null, runTip: journal && journal.tip ? journal.tip.hash : null,
|
|
391
|
+
builderId: process.env.COBOLWORK_BUILDER_ID || null,
|
|
392
|
+
...(process.env.SOURCE_DATE_EPOCH ? {} : { startedOn: STARTED, finishedOn: new Date().toISOString() }),
|
|
393
|
+
})
|
|
394
|
+
: result.provenance;
|
|
395
|
+
const text = JSON.stringify(record, null, 1) + '\n';
|
|
396
|
+
writeFileSync(resolve(opts.provenance), text);
|
|
397
|
+
recordOutput(journal, 'provenance', text, resolve(opts.provenance));
|
|
398
|
+
}
|
|
298
399
|
process.stderr.write(`${buildSummaryLine(result.doc)}\n`);
|
|
299
400
|
process.exitCode = result.exit;
|
|
401
|
+
recordVerdict(journal, result.doc, result.exit);
|
|
300
402
|
} else if (command === 'baseline') {
|
|
301
403
|
// A suppression nobody dated is never looked at again, so every part of the judgement is asked for.
|
|
302
404
|
const missing = ['reason', 'who', 'expires'].filter((k) => !opts[k]);
|
|
@@ -313,8 +415,14 @@ try {
|
|
|
313
415
|
if (held.problems.length) { process.stderr.write(`cobolwork: ${path} holds entries that do not validate, so it is left as it is:\n ${held.problems.join('\n ')}\n`); process.exit(2); }
|
|
314
416
|
const report = scanAll(root, { repos, only: opts.only, advisoryFeeds: opts.advisoryFeeds, systemDirs });
|
|
315
417
|
const { entries, added } = baselineEntries(report.findings, held.entries, { action, reason: opts.reason, who: opts.who, expires: expires.toISOString(), at, rules: opts.rule || null });
|
|
316
|
-
|
|
317
|
-
|
|
418
|
+
const written = JSON.stringify({ _comment: 'Judgements over cobolwork findings, matched by fingerprint. A suppression lapses at its expires date and the finding comes back.', entries }, null, 1) + '\n';
|
|
419
|
+
writeFileSync(path, written);
|
|
420
|
+
recordInputs(journal, 0, root);
|
|
421
|
+
recordFindings(journal, report);
|
|
422
|
+
recordBaselineWrite(journal, { before: held.entries, after: entries, who: opts.who, expires: expires.toISOString(), reason: opts.reason, path, text: written });
|
|
423
|
+
const line = `${JSON.stringify({ tool: 'cobolwork-baseline', path, added, entries: entries.length })}\n`;
|
|
424
|
+
recordOutput(journal, 'baseline', line);
|
|
425
|
+
process.stdout.write(line);
|
|
318
426
|
} else if (command === 'tui') {
|
|
319
427
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
320
428
|
process.stderr.write('cobolwork: tui needs a terminal on standard input and output; cobolwork scan writes the same report without one\n');
|
|
@@ -332,8 +440,15 @@ try {
|
|
|
332
440
|
const packet = explainFinding({ ...report, findings: [...report.findings, ...(report.suppressed || [])] }, fingerprint, { root });
|
|
333
441
|
if (!packet) { process.stderr.write(`cobolwork: no finding with fingerprint ${fingerprint} in this report\n`); process.exit(2); }
|
|
334
442
|
emit(packet, opts);
|
|
443
|
+
} else if (command === 'sbom') {
|
|
444
|
+
const bom = sbom(root, { systemDirs, name: opts.name });
|
|
445
|
+
recordInputs(journal, 0, root);
|
|
446
|
+
const text = JSON.stringify(bom, null, opts.quiet ? 0 : 1) + '\n';
|
|
447
|
+
recordOutput(journal, 'sbom', text, opts.out || null);
|
|
448
|
+
if (opts.out) writeFileSync(opts.out, text); else process.stdout.write(text);
|
|
335
449
|
} else if (command === 'inventory') {
|
|
336
450
|
const inv = inventory(root, { systemDirs });
|
|
451
|
+
recordInputs(journal, 0, root);
|
|
337
452
|
emit(inv, opts);
|
|
338
453
|
warnCoverage(inv);
|
|
339
454
|
} else if (command === 'parse') {
|
|
@@ -348,7 +463,9 @@ try {
|
|
|
348
463
|
process.stderr.write(`cobolwork: unknown command ${command}\n${USAGE}`);
|
|
349
464
|
process.exit(2);
|
|
350
465
|
}
|
|
466
|
+
finishEvidence(journal, process.exitCode ?? 0);
|
|
351
467
|
} catch (e) {
|
|
352
468
|
process.stderr.write(`cobolwork: ${printable(e && e.message ? e.message : e)}\n`);
|
|
469
|
+
finishEvidence(journal, 2);
|
|
353
470
|
process.exit(2);
|
|
354
471
|
}
|