@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/README.md
CHANGED
|
@@ -54,7 +54,7 @@ request and writes SARIF for code scanning: [docs/github-action.md](docs/github-
|
|
|
54
54
|
|
|
55
55
|
| Area | What is reported |
|
|
56
56
|
|---|---|
|
|
57
|
-
| Data flow | untrusted input - the command line, a job's `PARM` or in-stream data, a CICS terminal or web request - reaching an OS command, dynamic SQL, a dynamic `CALL`, `LINK` or `XCTL`, a file name, the internal reader, a subscript or length, decimal arithmetic, a record key or a log, across programs |
|
|
57
|
+
| Data flow | untrusted input - the command line, a job's `PARM` or in-stream data, a CICS terminal or web request, a CICS queue item - reaching an OS command, dynamic SQL, a dynamic `CALL`, `LINK` or `XCTL`, a file name, the internal reader, a subscript or length, decimal arithmetic, a record key or a log, across programs |
|
|
58
58
|
| Checks | a check counts only where it runs first, on every route; one that leaves the value safe for the sink clears the route |
|
|
59
59
|
| Screen fields | a field the BMS map protects, read back and used to choose a record: the 3270 hidden form field |
|
|
60
60
|
| Exfiltration | database rows and file records leaving through web calls, sockets, MQ, extrapartition queues or service calls |
|
|
@@ -63,13 +63,21 @@ request and writes SARIF for code scanning: [docs/github-action.md](docs/github-
|
|
|
63
63
|
| JCL | credentials and security commands in in-stream data, destructive statements, `DLM=` tricks, FTP in cleartext or sending production data, production data touched by a test job |
|
|
64
64
|
| The source | names nothing declares (code that cannot compile), shadowed copybooks, payloads hidden in columns 73-80 or aimed at AI readers |
|
|
65
65
|
| The estate | production names outside production jobs, routable addresses, compiler and runtime versions with published advisories |
|
|
66
|
-
| Assembler | a switch to key zero or supervisor state, an instruction run through `EX`, cross-memory calls, the security product called directly, and the CSECT or ENTRY a COBOL `CALL` or a job step reaches |
|
|
66
|
+
| Assembler | a switch to key zero or supervisor state, an instruction run through `EX`, cross-memory calls, a module named at run time, the security product called directly, and the CSECT or ENTRY a COBOL `CALL` or a job step reaches |
|
|
67
|
+
| IMS and Db2 | a PSB granting a program an option its own DL/I calls never use, and a segment a DL/I get retrieves reaching a sink; a PSB letting a program change every segment it reads, a SENSEG naming no segment of its DBD, a `KEYLEN` or field that DBDGEN, PSBGEN or ACBGEN refuses; a GRANT to `PUBLIC`, `WITH GRANT OPTION` or of a system authority, and the exit routines and load modules that run inside Db2 |
|
|
68
|
+
| PL/I | input reaching a sink through a PL/I program, as in COBOL; a `FETCH` of a module named at run time, an `ON` unit that ignores its condition, and where pointers, entry variables or the preprocessor take the flow out of sight |
|
|
67
69
|
| Cryptography | a single-length DES key, an MD5 or SHA-1 hash, or a fixed initialization vector asked of ICSF, read from IBM's parameter lists; an outbound CICS connection asking for HTTP |
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
| Secrets | a credential written into a program or copybook: a literal `VALUE` on an item named for one, or a literal password in `EXEC SQL CONNECT` or `EXEC CICS SIGNON` |
|
|
71
|
+
|
|
72
|
+
HLASM is read, not assembled. Each statement is parsed by what its operation is: a machine
|
|
73
|
+
instruction against IBM's operand syntax for its mnemonic (`rules/hlasm-instructions.json`, every
|
|
74
|
+
z/Architecture mnemonic with its page in the Principles of Operation), an assembler instruction,
|
|
75
|
+
or a z/OS, Language Environment or HLASM Toolkit macro against the keywords IBM lists. The location
|
|
76
|
+
and length of each symbol is worked out as the assembler would, and nothing after a statement of
|
|
77
|
+
unknown length is placed. Macros are not expanded and conditional assembly is not evaluated, so an
|
|
78
|
+
operation a site macro issues is seen where the macro is defined, and not where it is used. A scan
|
|
79
|
+
names each macro it did not expand, each COPY member the tree does not hold, and each statement it
|
|
80
|
+
could not read.
|
|
73
81
|
|
|
74
82
|
[docs/rule-sets.md](docs/rule-sets.md) describes each in full, with what it deliberately leaves out.
|
|
75
83
|
Vendor packs for CA ACF2 and Top Secret, Control-M and Connect:Direct load only for estates that
|
|
@@ -127,7 +135,10 @@ language, are not in it.
|
|
|
127
135
|
and the paragraph or section it sits in (the job, step and DD for JCL), and the flagged statement's
|
|
128
136
|
own text. No line number goes into it, so code added above a finding does not change it. `diff`
|
|
129
137
|
compares findings by it, and SARIF carries it as `partialFingerprints["cobolwork/v1"]`. Two findings
|
|
130
|
-
that only their position tells apart share one, and `summary.identity.shared` counts them.
|
|
138
|
+
that only their position tells apart share one, and `summary.identity.shared` counts them. What goes
|
|
139
|
+
into a fingerprint changes only in a major release, under a new version. [STABILITY.md](STABILITY.md)
|
|
140
|
+
lists that and the other contracts a release keeps. `schema/cobolwork-finding.schema.json` describes
|
|
141
|
+
a finding, and `schema/` holds a schema for every document cobolwork writes or reads.
|
|
131
142
|
|
|
132
143
|
### Findings and Claim Severity
|
|
133
144
|
|
|
@@ -215,8 +226,13 @@ entered ([docs/spec/evidence.md](docs/spec/evidence.md) §13.5).
|
|
|
215
226
|
|
|
216
227
|
`COBOLWORK_ABENDS` names fuzz runs ironwork's harness wrote. Each abend an input caused becomes a
|
|
217
228
|
finding at the line it happened, with the input and the run's journal, once that journal verifies:
|
|
218
|
-
`input-causes-abend-s0c7`, `-s0c4
|
|
219
|
-
(
|
|
229
|
+
`input-causes-abend-s0c7`, `-s0c4` (in a CICS task, the ASRA whose message names that check),
|
|
230
|
+
`-subscript-range`, `input-causes-hang` for an input that keeps a loop running past the statement limit (S322), `input-selects-program`
|
|
231
|
+
for a CALL of a missing program (U4038 with CEE3501S, in a CICS task transaction abend 4038, or S806 from earlier ironwork releases) whose journal shows the input reaching the CALL, or `input-causes-abend` for any other
|
|
232
|
+
code ([docs/spec/evidence.md](docs/spec/evidence.md) §13.6). It reads manifests in format
|
|
233
|
+
`ironwork-fuzz/v1`, and `ironwork-fuzz-interface/v1` from a subprogram fuzzed with generated
|
|
234
|
+
arguments, whose findings say no caller run shows a caller passes them and warn by default; it
|
|
235
|
+
reports one in any other format as a problem instead of guessing at it.
|
|
220
236
|
|
|
221
237
|
### What each finding lets someone do, and the fix
|
|
222
238
|
|
|
@@ -285,7 +301,10 @@ clean result.
|
|
|
285
301
|
Some rules need facts no repository holds: which dataset qualifiers are production, which DDs reach
|
|
286
302
|
the internal reader, the compiler options and runtime versions in use, which libraries are
|
|
287
303
|
authorised. They go in `cobolwork.site.json`, and `node diag/propose-site.mjs <path>` drafts one from
|
|
288
|
-
the estate's own JCL for a person to correct.
|
|
304
|
+
the estate's own JCL for a person to correct. `schema/cobolwork.site.schema.json` describes the
|
|
305
|
+
file. Its `version` is 1, and a file without one is read as version 0, which holds the same keys. A
|
|
306
|
+
key cobolwork does not read is named under `summary.siteWarnings` rather than dropped, and a key
|
|
307
|
+
opening with `_` is a note for people. Without a fact, the rule that needs it says it did not
|
|
289
308
|
run, under `setsIncomplete`, rather than reporting a clean result. The benchmark tree has no site
|
|
290
309
|
file, which is why three sets say so above. `advisoryCoverage` names the products the advisory rules
|
|
291
310
|
searched, so a scan with no advisory finding says what that silence covers.
|
|
@@ -329,26 +348,65 @@ The parser is graded against GnuCOBOL's own listing (`cobc -t -Xref -ftsymbols`)
|
|
|
329
348
|
data item with the size the compiler computed, every label, called programs, and which references
|
|
330
349
|
write to a field. `diag/grade-against-gnucobol.mjs` runs that comparison over a corpus, on
|
|
331
350
|
repositories never used while building the parser. The 100 and 300 sets were measured 2026-09-18,
|
|
332
|
-
the 500 set 2026-
|
|
351
|
+
the 500 set 2026-10-04:
|
|
333
352
|
|
|
334
353
|
| Corpus | Files the compiler accepted | Data items | Sizes | Labels and calls |
|
|
335
354
|
|---|---|---|---|---|
|
|
336
355
|
| 100 repositories, held out | 489 | 100% recall, 100% precision | 0 disagree of 15,888 | 100% |
|
|
337
356
|
| 300 repositories, held out | 2,210 | 99.9% / 100% | 8 disagree of 94,786 | 100% / 99.7% |
|
|
338
|
-
| 500 repositories, held out | 21,
|
|
357
|
+
| 500 repositories, held out | 21,707 | 100% / 100% | 0 disagree of 719,184 | 100% / 100% |
|
|
339
358
|
|
|
340
|
-
On the 500 set
|
|
341
|
-
|
|
342
|
-
GnuCOBOL accepts, so a program with EXEC SQL or EXEC CICS is graded only
|
|
343
|
-
stand-in in `diag/precompiler.mjs`, which rewrites what the parser would
|
|
344
|
-
tests compare the parser with the compiler's answers kept in `test/fixtures/parser/*.golden.json`,
|
|
359
|
+
On the 500 set every data item, size and call agrees with the compiler: 845,704 items, 27,703
|
|
360
|
+
calls. Of 146,004 labels one differs, an `EJECT` written in Area A, which GnuCOBOL reads as a
|
|
361
|
+
paragraph and IBM as the listing directive it is. The grade covers only programs GnuCOBOL accepts, so a program with EXEC SQL or EXEC CICS is graded only
|
|
362
|
+
through the precompiler stand-in in `diag/precompiler.mjs`, which rewrites what the parser would
|
|
363
|
+
otherwise have to read. The tests compare the parser with the compiler's answers kept in `test/fixtures/parser/*.golden.json`,
|
|
345
364
|
so they run without GnuCOBOL.
|
|
346
365
|
|
|
347
|
-
|
|
366
|
+
HLASM is graded against z390, an HLASM-compatible assembler, by `diag/hlasm-oracle.mjs` and
|
|
367
|
+
`diag/hlasm-grade-z390.mjs`. Each file is assembled twice, the second time with sixteen bytes after
|
|
368
|
+
every macro call, and only values the two runs agree on are graded. None of them depends on how
|
|
369
|
+
z390's macros, or IBM's, expand. Measured 2026-10-03 over the files z390 assembles:
|
|
370
|
+
|
|
371
|
+
| Corpus | Files | Symbol locations | Statement locations | Instruction lengths | External names |
|
|
372
|
+
|---|---|---|---|---|---|
|
|
373
|
+
| 12 repositories, dev | 48 | 935 agree of 936, 0 differ | 2,511 of 2,546, 0 differ | 2,273 of 2,273 | 55 of 55 |
|
|
374
|
+
| 17 repositories, held out | 76 | 546 agree of 557, 0 differ | 1,631 of 1,675, 0 differ | 1,710 of 1,710 | 90 of 92 |
|
|
375
|
+
|
|
376
|
+
A value not agreeing was not placed, never placed differently: it follows a statement of unknown
|
|
377
|
+
length. Two of z390's values are not graded, because it differs from the Language Reference, which
|
|
378
|
+
the reader follows: an EQU's length, where z390 gives 1 and the reference the length of the
|
|
379
|
+
leftmost term; and anything after a literal pool where z390 pads a literal that the reference packs.
|
|
380
|
+
A file with an error that can move a location is not graded either, such as an undefined symbol in
|
|
381
|
+
a statement holding a literal, which z390 then leaves out of the pool. Of the statements outside conditional
|
|
382
|
+
assembly, 99.5% parse on the dev corpus and 99.8% on the held-out one; the rest are counted by
|
|
383
|
+
kind. `bench/hlasm-locate/` holds small programs, one assembler feature each, with z390's answers
|
|
384
|
+
recorded beside them, so the tests check the locator without z390.
|
|
385
|
+
|
|
386
|
+
PL/I has no compiler that may grade it, so its reader is measured by how much of a corpus it parses
|
|
387
|
+
(`diag/pli-measure.mjs`, over `.pli`, `.pl1` and `.plx` files and `.inc` and `.pcx` include
|
|
388
|
+
members), counted by statement kind, with every statement it does not parse named.
|
|
389
|
+
The 500 set was used while building the reader. One of its repositories, `jcf608_PacificNationalBank`,
|
|
390
|
+
is generated code and is counted apart. Measured 2026-10-07:
|
|
391
|
+
|
|
392
|
+
| Corpus | Repositories | Files | Statements | Parsed |
|
|
393
|
+
|---|---|---|---|---|
|
|
394
|
+
| 500 repositories, dev, written by hand | 9 | 164 | 9,547 | 98.8% |
|
|
395
|
+
| 500 repositories, dev, generated | 1 | 901 | 263,531 | 99.9% |
|
|
396
|
+
| 200 repositories, held out | 3 | 31 | 1,768 | 99.8% |
|
|
397
|
+
| 300 repositories, held out | 6 | 66 | 3,686 | 99.8% |
|
|
398
|
+
|
|
399
|
+
Two repositories parse nothing, because the members measured there are `.inc` files that are not
|
|
400
|
+
PL/I (three statements each). `jarora8_GitPlay` parses 89%, and most of the rest of
|
|
401
|
+
the dev set's misses are its statements. Structure offsets are checked against hand-built fixtures
|
|
402
|
+
and the Language Reference's own example, not against a compiler.
|
|
403
|
+
|
|
404
|
+
`bench/cases/` holds 153 CWE-labelled cases, each paired with a near-miss negative: the same shape
|
|
348
405
|
with the flaw removed. `node bench/run.mjs` scores any scanner's findings against them, by rule and
|
|
349
406
|
file, never by line, and `npm test` fails if any case scores differently from its declaration.
|
|
350
|
-
`--validate` compiles every COBOL case with GnuCOBOL
|
|
351
|
-
|
|
407
|
+
`--validate` compiles every COBOL case with GnuCOBOL, assembles every HLASM case with z390 when
|
|
408
|
+
`Z390` names a release (stubbing the macros z390 does not ship), and checks every JCL case against
|
|
409
|
+
the statement grammar, a weaker witness, and says so.
|
|
352
410
|
|
|
353
411
|
## Coverage on a busy machine
|
|
354
412
|
|
|
@@ -375,10 +433,10 @@ and to a COBIT 2019 practice by identifier alone:
|
|
|
375
433
|
|
|
376
434
|
| File | Instrument | Rules mapped |
|
|
377
435
|
|---|---|---|
|
|
378
|
-
| `rules/compliance-dora.json` | Regulation (EU) 2022/2554 (DORA) |
|
|
379
|
-
| `rules/compliance-ffiec.json` | FFIEC IT Examination Handbook |
|
|
380
|
-
| `rules/compliance-nist80053.json` | NIST SP 800-53 Rev. 5.2.0 |
|
|
381
|
-
| `rules/compliance-cobit2019.json` | COBIT 2019 (ISACA), identifiers only |
|
|
436
|
+
| `rules/compliance-dora.json` | Regulation (EU) 2022/2554 (DORA) | 249 |
|
|
437
|
+
| `rules/compliance-ffiec.json` | FFIEC IT Examination Handbook | 247, and 2 recorded as unmapped |
|
|
438
|
+
| `rules/compliance-nist80053.json` | NIST SP 800-53 Rev. 5.2.0 | 247, and 2 recorded as unmapped |
|
|
439
|
+
| `rules/compliance-cobit2019.json` | COBIT 2019 (ISACA), identifiers only | 247, and 2 recorded as unmapped |
|
|
382
440
|
|
|
383
441
|
A COBIT 2019 row names the objective or practice and gives this project's own rationale; no ISACA
|
|
384
442
|
text is reproduced, so reading what a practice says needs a copy of the framework. The practice is
|
|
@@ -386,12 +444,17 @@ chosen at the NIST control, through a crosswalk in `diag/map-compliance.mjs`, so
|
|
|
386
444
|
from the rule's NIST clause.
|
|
387
445
|
|
|
388
446
|
`node diag/map-compliance.mjs` refuses to write a quote the cached instrument does not contain, and
|
|
389
|
-
every scan carries the mapping as `ruleCompliance`. The clause choice is a judgement that no
|
|
447
|
+
every scan carries the mapping as `ruleCompliance`, and SARIF on each rule descriptor as `properties.compliance`. The clause choice is a judgement that no
|
|
390
448
|
qualified assessor has reviewed. Where no control genuinely covers a rule, as for committing an LPAR
|
|
391
449
|
name to a repository, the rule is recorded as unmapped with a reason rather than mapped to the
|
|
392
450
|
nearest control that reads plausibly. What else the mapping does not claim is in
|
|
393
451
|
[docs/rule-sets.md](docs/rule-sets.md#compliance).
|
|
394
452
|
|
|
453
|
+
The same files map the evidence cobolwork and ironwork write: a run journal's records, its chain,
|
|
454
|
+
the provenance statement and ironwork's options in force, each to its NIST control and COBIT
|
|
455
|
+
practice, and `evidence verify` names the clauses of what it checked
|
|
456
|
+
([docs/spec/evidence.md](docs/spec/evidence.md) §9).
|
|
457
|
+
|
|
395
458
|
## Tests
|
|
396
459
|
|
|
397
460
|
npm test
|
package/STABILITY.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Stability
|
|
2
|
+
|
|
3
|
+
What a cobolwork release keeps from the release before it.
|
|
4
|
+
|
|
5
|
+
From 0.9.0, the contracts below change only in a major release. Until then a minor release may
|
|
6
|
+
change any of them, and its release notes say so.
|
|
7
|
+
|
|
8
|
+
## The contracts
|
|
9
|
+
|
|
10
|
+
- **Rule ids.** Each rule a rule set declares, as `cobolwork capabilities` and the SARIF tool
|
|
11
|
+
components list them.
|
|
12
|
+
- **The command line.** Each command with its arguments and options, as `cobolwork capabilities`
|
|
13
|
+
lists them under `commands` and `globalOptions`.
|
|
14
|
+
- **Exit statuses**, below.
|
|
15
|
+
- **Documents and their schemas**, below.
|
|
16
|
+
- **The fingerprint, `cobolwork/v1`.** What goes into a finding's fingerprint and pairing keys.
|
|
17
|
+
`test/identity-golden.test.mjs` pins the hashes, so a change to the inputs fails it.
|
|
18
|
+
|
|
19
|
+
The JavaScript exports of `lib/` are not covered.
|
|
20
|
+
|
|
21
|
+
## Exit statuses
|
|
22
|
+
|
|
23
|
+
| Command | Status |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Every command | 0 done; 2 a usage error, or the command could not run |
|
|
26
|
+
| `build` | 0 pass, 1 fail, 3 undecided, 4 the compiler failed after a pass, 2 could not run |
|
|
27
|
+
| `gate --exit-code` | 0 pass, 1 fail, 3 undecided, 2 could not run |
|
|
28
|
+
| `evidence verify` | 0 verified and sealed, 1 broken or not sealed, 3 undetermined, 2 a usage error |
|
|
29
|
+
|
|
30
|
+
## Documents
|
|
31
|
+
|
|
32
|
+
Each JSON document cobolwork writes names itself in `tool` and gives its version in
|
|
33
|
+
`schemaVersion`. `cobolwork capabilities` lists both under `documents`. Each schema describes every
|
|
34
|
+
key its document carries at the top level, the scan, flow and diff schemas every key in `summary` and
|
|
35
|
+
in each finding too, and a test holds the documents cobolwork writes to them.
|
|
36
|
+
|
|
37
|
+
| Document | Version | Schema |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `cobolwork` (`scan`) | 3 | `schema/cobolwork-report.schema.json`, each finding `schema/cobolwork-finding.schema.json` |
|
|
40
|
+
| `cobolwork-flow` | 3 | `schema/cobolwork-flow.schema.json` |
|
|
41
|
+
| `cobolwork-inventory` | 3 | `schema/cobolwork-inventory.schema.json` |
|
|
42
|
+
| `cobolwork-diff` | 3 | `schema/cobolwork-diff.schema.json` |
|
|
43
|
+
| `cobolwork-gate` | 3 | `schema/cobolwork-gate.schema.json` |
|
|
44
|
+
| `cobolwork-build` | 1 | `schema/cobolwork-build.schema.json` |
|
|
45
|
+
| `cobolwork-build-provenance` | 1 | `schema/cobolwork-build-provenance.schema.json` |
|
|
46
|
+
| `cobolwork-capabilities` | 1 | `schema/cobolwork-capabilities.schema.json` |
|
|
47
|
+
| `cobolwork-explain` | 1 | `schema/cobolwork-explain.schema.json` |
|
|
48
|
+
| `cobolwork-parse` | 1 | `schema/cobolwork-parse.schema.json` |
|
|
49
|
+
| `cobolwork-baseline` | 1 | `schema/cobolwork-baseline.schema.json` |
|
|
50
|
+
| `cobolwork-evidence` | 1 | `schema/cobolwork-evidence.schema.json` |
|
|
51
|
+
| SARIF | 2.1.0 | The coverage property bag: `schema/cobolwork-coverage.schema.json` |
|
|
52
|
+
| CycloneDX SBOM | 1.6 | |
|
|
53
|
+
| Evidence records | `cobolwork-evidence/v1` | `docs/spec/evidence.md`; record kinds in `test/fixtures/evidence/kinds.tsv` |
|
|
54
|
+
|
|
55
|
+
The files cobolwork reads:
|
|
56
|
+
|
|
57
|
+
| File | Version key | Schema |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `cobolwork.site.json` | `version`: 1 | `schema/cobolwork.site.schema.json` |
|
|
60
|
+
| Build policy | `policyVersion`: 1 | `schema/cobolwork.policy.schema.json` |
|
|
61
|
+
| `cobolwork.baseline.json` | `version`: 1 | `schema/cobolwork.baseline.schema.json` |
|
|
62
|
+
| Witness feed (`COBOLWORK_WITNESS`) | `version`: 1 | `schema/cobolwork-witness.schema.json` |
|
|
63
|
+
| Reachability extract (`COBOLWORK_REACH`) | `version`: 1 | `schema/cobolwork-reach.schema.json` |
|
|
64
|
+
| Execution coverage (`COBOLWORK_EXECUTION`) | ironwork's | `schema/cobolwork-execution.schema.json`, the keys read |
|
|
65
|
+
|
|
66
|
+
A file of cobolwork's own without its version key is read as version 0, which holds the same keys
|
|
67
|
+
as version 1. A newer version than the release reads is refused, and the message names both
|
|
68
|
+
versions. A key the site file does not hold is named under `summary.siteWarnings`, and one a witness
|
|
69
|
+
feed or reachability extract does not hold under `summary.feedWarnings`.
|
|
70
|
+
|
|
71
|
+
## How a contract changes
|
|
72
|
+
|
|
73
|
+
- **Additions** come in a minor release: a rule, a command, an option, or a key in a document
|
|
74
|
+
cobolwork writes. A program that reads cobolwork's documents should ignore keys it does not know.
|
|
75
|
+
- **A rename** keeps the old name working as an alias for one major release, and using it warns.
|
|
76
|
+
- **A deprecation** warns for at least one minor release before the major release that removes it.
|
package/bin/cobolwork.mjs
CHANGED
|
@@ -14,12 +14,13 @@ import { diffRefs } from '../lib/diff.mjs';
|
|
|
14
14
|
import { gateRefs, VERDICT_EXIT } from '../lib/gate.mjs';
|
|
15
15
|
import { build, buildSarif, buildSummaryLine } from '../lib/build.mjs';
|
|
16
16
|
import { capabilities } from '../lib/capabilities.mjs';
|
|
17
|
+
import { PARSE_SCHEMA_VERSION, BASELINE_RESULT_SCHEMA_VERSION } from '../lib/version.mjs';
|
|
17
18
|
import { commitAt, revisionOf, toolRevision } from '../lib/revision.mjs';
|
|
18
19
|
import { stampFingerprints } from '../lib/kernel/identity.mjs';
|
|
19
20
|
import { pdsExportTree } from '../lib/kernel/source-tree.mjs';
|
|
20
21
|
import { SITE_FILE } from '../lib/site.mjs';
|
|
21
22
|
import { tally } from '../lib/kernel/findings.mjs';
|
|
22
|
-
import { loadBaseline, applyBaseline, baselineEntries, BASELINE_FILE, SUPPRESSING } from '../lib/baseline.mjs';
|
|
23
|
+
import { loadBaseline, applyBaseline, baselineEntries, BASELINE_FILE, BASELINE_VERSION, SUPPRESSING } from '../lib/baseline.mjs';
|
|
23
24
|
import { readReport } from '../lib/tui/model.mjs';
|
|
24
25
|
import { nodeTerminal } from '../lib/tui/terminal.mjs';
|
|
25
26
|
import { runTui } from '../lib/tui/run.mjs';
|
|
@@ -28,7 +29,7 @@ import { printable } from '../lib/kernel/printable.mjs';
|
|
|
28
29
|
import { startEvidence, recordInputs, recordHashed, recordFindings, recordOutput, recordVerdict, recordBaselineWrite, finishEvidence } from '../lib/evidence/run.mjs';
|
|
29
30
|
import { evidenceCommand } from '../lib/evidence/cli.mjs';
|
|
30
31
|
import { slsaStatement } from '../lib/evidence/slsa.mjs';
|
|
31
|
-
import {
|
|
32
|
+
import { optableLevel } from '../lib/hlasm/optable.mjs';
|
|
32
33
|
|
|
33
34
|
const STARTED = new Date().toISOString();
|
|
34
35
|
|
|
@@ -60,7 +61,7 @@ const USAGE = `cobolwork ${VERSION} — COBOL, JCL and CICS security analysis, n
|
|
|
60
61
|
what this cobolwork can do: commands and options, the version of every
|
|
61
62
|
document it writes, its fingerprint version, the file kinds it reads, and
|
|
62
63
|
the commit it runs from
|
|
63
|
-
cobolwork build <repo> [--base <ref>] [--policy <file>] [--provenance <file>] [--ironwork <path> | -- <compiler> <arg>...]
|
|
64
|
+
cobolwork build <repo> [--base <ref>] [--policy <file>] [--provenance <file>] [--ironwork <path> | [--precompile] -- <compiler> <arg>...]
|
|
64
65
|
the build gate: every finding ranked LOW to KNOWN-EXPLOITABLE, the
|
|
65
66
|
policy's blocking findings and compiler options checked, and the
|
|
66
67
|
compiler run only on a pass. Exits 0 pass, 1 fail, 3 undecided, 4 the
|
|
@@ -92,6 +93,13 @@ Options
|
|
|
92
93
|
--pds-export scan: the directory holds partitioned data sets' members as files, as
|
|
93
94
|
zowe zos-files download all-members writes them (hlq/llq/member.txt) or in a
|
|
94
95
|
directory named for each data set; findings name DATA.SET/MEMBER
|
|
96
|
+
--mvs38-forms, --no-mvs38-forms scan: read (the default) or refuse the operands MVS 3.8's system
|
|
97
|
+
macros take and z/OS 3.1's documentation does not list, such as MODESET
|
|
98
|
+
EXTKEY=SUPR (key zero), GETMAIN P and ATTACH HIARCHY=
|
|
99
|
+
--hlasm-optable <table> scan: read assembler source with this HLASM operation code table (UNI,
|
|
100
|
+
DOS, 370, XA, ESA, ZOP, YOP, Z9 ... Z17, or a MACHINE name such as S390): a
|
|
101
|
+
mnemonic outside it is a macro call. Without it the estate's assembly JCL
|
|
102
|
+
PARM or a file's *PROCESS decides, and UNI otherwise
|
|
95
103
|
--copylib <dir>[,<dir>] copy libraries the estate keeps outside the repository, searched after the
|
|
96
104
|
tree's own copybooks, as COBCPY is; a copybook found there is read, not reported missing
|
|
97
105
|
--report <file> tui, explain: read a stored scan report instead of scanning
|
|
@@ -108,18 +116,32 @@ Options
|
|
|
108
116
|
the SLSA Provenance v1 predicate (unsigned; the pipeline signs it)
|
|
109
117
|
--artifact <path>[,<path>] build: what the compiler produced, named as subjects of the SLSA statement
|
|
110
118
|
--equivalence <file>[,<file>] build --base: ironwork equivalence statements for the programs the
|
|
111
|
-
change edits
|
|
112
|
-
requireEquivalence (never, machineAuthored, always) says when one is required
|
|
119
|
+
change edits, each signed by one of --allowed-signers. The policy's
|
|
120
|
+
requireEquivalence (never, machineAuthored, always) says when one is required;
|
|
121
|
+
where one may be, a build with no --allowed-signers fails the check
|
|
113
122
|
--ironwork <path> build: after a pass, run ironwork check on every program, for an estate that
|
|
114
123
|
compiles with IBM Enterprise COBOL; a program ironwork rejects exits 4, one it
|
|
115
124
|
does not model yet leaves the build undecided
|
|
125
|
+
--precompile build -- <compiler>: after a pass, translate each program the compiler command
|
|
126
|
+
names that holds EXEC SQL or EXEC CICS and run the compiler on it with
|
|
127
|
+
-fsyntax-only; one it refuses exits 4 and the command after -- does not run
|
|
116
128
|
--evidence <dir> scan, flow, diff, gate, build, baseline, inventory: record this run in a
|
|
117
129
|
hash-chained journal and ledger there (or COBOLWORK_EVIDENCE); never inside
|
|
118
130
|
the tree being read
|
|
119
|
-
--allowed-signers <file> evidence verify: OpenSSH allowed_signers for namespace
|
|
131
|
+
--allowed-signers <file> evidence verify and build: OpenSSH allowed_signers for namespace
|
|
132
|
+
cobolwork-evidence; build refuses one inside the repository
|
|
120
133
|
--anchor-git <repo>, --ref <ref>, --push, --max-unsealed <n>
|
|
121
134
|
evidence: the git witness, the ref that counts (default @{upstream}), whether
|
|
122
135
|
anchor pushes, and how many ledger records may follow the newest seal
|
|
136
|
+
--anchor-pin <commit> evidence verify: the witness commit an earlier verify reported as
|
|
137
|
+
witnessCommit; the witness must still hold every seal it held
|
|
138
|
+
--tsq <file>, --tsr <file>, --tsa-ca <file>
|
|
139
|
+
evidence anchor writes an RFC 3161 request (and keeps it beside the seal);
|
|
140
|
+
verify reads the response, checking imprint and nonce, and has OpenSSL check
|
|
141
|
+
the authority's signature against the CA certificate
|
|
142
|
+
--cosign-bundle <file> with --cosign-key <file> or --certificate-identity <id> and
|
|
143
|
+
--certificate-oidc-issuer <url>, [--trusted-root <file>] [--insecure-ignore-tlog]
|
|
144
|
+
evidence verify: a transparency-log bundle over a seal, checked by cosign
|
|
123
145
|
|
|
124
146
|
Exit codes: 0 the command ran, 2 it could not run. A run that examined nothing says so in
|
|
125
147
|
summary.filesScanned and summary.nosrc rather than reporting a clean zero.
|
|
@@ -158,6 +180,10 @@ function parseArgs(argv) {
|
|
|
158
180
|
else if (a === '--rule') opts.rule = list();
|
|
159
181
|
else if (a === '--advisories') opts.advisoryFeeds = [...new Set(list().map(x => resolve(x)))];
|
|
160
182
|
else if (a === '--pds-export') opts.pdsExport = true;
|
|
183
|
+
else if (a === '--mvs38-forms') opts.mvs38Forms = true;
|
|
184
|
+
else if (a === '--no-mvs38-forms') opts.mvs38Forms = false;
|
|
185
|
+
else if (a === '--hlasm-optable') opts.hlasmOptable = value();
|
|
186
|
+
else if (a === '--precompile') opts.precompile = true;
|
|
161
187
|
else if (a === '--copylib') opts.copylib = [...new Set(list().map(x => resolve(x)))];
|
|
162
188
|
else if (a === '--report') opts.report = value();
|
|
163
189
|
else if (a === '--keys') opts.keys = value();
|
|
@@ -182,6 +208,15 @@ function parseArgs(argv) {
|
|
|
182
208
|
else if (a === '--push') opts.push = true;
|
|
183
209
|
else if (a === '--max-unsealed') opts.maxUnsealed = value();
|
|
184
210
|
else if (a === '--tsq') opts.tsq = value();
|
|
211
|
+
else if (a === '--anchor-pin') opts.anchorPin = value();
|
|
212
|
+
else if (a === '--tsr') opts.tsr = value();
|
|
213
|
+
else if (a === '--tsa-ca') opts.tsaCa = value();
|
|
214
|
+
else if (a === '--cosign-bundle') opts.cosignBundle = value();
|
|
215
|
+
else if (a === '--certificate-identity') opts.certificateIdentity = value();
|
|
216
|
+
else if (a === '--certificate-oidc-issuer') opts.certificateOidcIssuer = value();
|
|
217
|
+
else if (a === '--cosign-key') opts.cosignKey = value();
|
|
218
|
+
else if (a === '--trusted-root') opts.trustedRoot = value();
|
|
219
|
+
else if (a === '--insecure-ignore-tlog') opts.insecureIgnoreTlog = true;
|
|
185
220
|
else if (a === '--expect-key') opts.expectKey = value();
|
|
186
221
|
else if (a === '--help' || a === '-h') opts.help = true;
|
|
187
222
|
else if (a === '--version' || a === '-v') opts.version = true;
|
|
@@ -275,7 +310,14 @@ if (opts.copylib && opts._.length && !COPYLIB_COMMANDS.includes(opts._[0])) {
|
|
|
275
310
|
const notDirs = (opts.copylib || []).filter((d) => { try { return !statSync(d).isDirectory(); } catch { return true; } });
|
|
276
311
|
if (notDirs.length) { process.stderr.write(`cobolwork: --copylib ${notDirs.join(', ')} is not a directory\n`); process.exit(2); }
|
|
277
312
|
const systemDirs = opts.copylib || [];
|
|
278
|
-
if (opts.pdsExport && opts._.length && opts._[0]
|
|
313
|
+
if (opts.pdsExport && opts._.length && !['scan', 'diff', 'build'].includes(opts._[0])) { process.stderr.write('cobolwork: --pds-export is for scan, diff and build\n'); process.exit(2); }
|
|
314
|
+
if (opts.hlasmOptable !== undefined) {
|
|
315
|
+
const level = optableLevel(opts.hlasmOptable) ?? optableLevel(opts.hlasmOptable, { machine: true });
|
|
316
|
+
if (!level) { process.stderr.write(`cobolwork: --hlasm-optable ${opts.hlasmOptable} names no HLASM operation code table\n`); process.exit(2); }
|
|
317
|
+
if (opts._.length && opts._[0] !== 'scan') { process.stderr.write('cobolwork: --hlasm-optable is for scan\n'); process.exit(2); }
|
|
318
|
+
opts.hlasmOptable = level;
|
|
319
|
+
}
|
|
320
|
+
if (opts.mvs38Forms !== undefined && opts._.length && opts._[0] !== 'scan') { process.stderr.write('cobolwork: --mvs38-forms and --no-mvs38-forms are for scan\n'); process.exit(2); }
|
|
279
321
|
if (opts.pdsExport && opts.repos) { process.stderr.write('cobolwork: --pds-export reads one export; --repos does not apply\n'); process.exit(2); }
|
|
280
322
|
if (opts.json && opts._.length && opts._[0] !== 'capabilities') {
|
|
281
323
|
process.stderr.write(`cobolwork: --json is for capabilities; every other command writes JSON unless --format says otherwise\n`);
|
|
@@ -297,7 +339,7 @@ if (opts.evidence !== undefined && opts._.length && ![...JOURNALED, 'evidence'].
|
|
|
297
339
|
process.stderr.write(`cobolwork: ${opts._[0]} records no evidence; ${JOURNALED.join(', ')} and evidence do\n`);
|
|
298
340
|
process.exit(2);
|
|
299
341
|
}
|
|
300
|
-
const evidenceFlag = ['sshKey', 'signer', 'allowedSigners', 'anchorGit', 'ref', 'push', 'maxUnsealed', 'tsq', 'expectKey'].find((k) => opts[k] !== undefined);
|
|
342
|
+
const evidenceFlag = ['sshKey', 'signer', 'allowedSigners', 'anchorGit', 'ref', 'push', 'maxUnsealed', 'tsq', 'expectKey', 'anchorPin', 'tsr', 'tsaCa', 'cosignBundle', 'certificateIdentity', 'certificateOidcIssuer', 'cosignKey', 'trustedRoot', 'insecureIgnoreTlog'].find((k) => opts[k] !== undefined);
|
|
301
343
|
if (opts.equivalence && !opts.base) { process.stderr.write('cobolwork: --equivalence judges a change; it needs --base\n'); process.exit(2); }
|
|
302
344
|
if (evidenceFlag && opts._.length && opts._[0] !== 'evidence' && !(evidenceFlag === 'allowedSigners' && opts._[0] === 'build')) {
|
|
303
345
|
process.stderr.write(`cobolwork: --${evidenceFlag.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)} is for evidence only\n`);
|
|
@@ -346,7 +388,7 @@ try {
|
|
|
346
388
|
} else if (command === 'evidence') {
|
|
347
389
|
process.exitCode = evidenceCommand(target, opts, { toolVersion: VERSION, write: (s) => process.stdout.write(s) });
|
|
348
390
|
} else if (command === 'scan' || command === 'flow') {
|
|
349
|
-
const flowOpts = { repos, fullTrace: opts.fullTrace === true, allRoutes: opts.allRoutes === true, systemDirs };
|
|
391
|
+
const flowOpts = { repos, fullTrace: opts.fullTrace === true, allRoutes: opts.allRoutes === true, systemDirs, mvs38Forms: opts.mvs38Forms !== false, hlasmOptable: opts.hlasmOptable ?? null };
|
|
350
392
|
// The site file sits beside the members and is not one of them, so it is named rather than found.
|
|
351
393
|
const site = resolve(root, SITE_FILE);
|
|
352
394
|
const pds = opts.pdsExport ? pdsExportTree(root, { systemDirs }) : null;
|
|
@@ -371,7 +413,7 @@ try {
|
|
|
371
413
|
warnCoverage(report);
|
|
372
414
|
} else if (command === 'diff') {
|
|
373
415
|
if (!opts.base) { process.stderr.write(`cobolwork: diff needs --base <ref>\n`); process.exit(2); }
|
|
374
|
-
const report = diffRefs(root, opts.base, opts.head || null, { only: opts.only, fullTrace: opts.fullTrace === true, systemDirs });
|
|
416
|
+
const report = diffRefs(root, opts.base, opts.head || null, { only: opts.only, fullTrace: opts.fullTrace === true, systemDirs, pdsExport: opts.pdsExport === true });
|
|
375
417
|
stampRevisions(report.summary, opts.head || null);
|
|
376
418
|
recordFindings(journal, { findings: [...report.findings, ...(report.introduced || [])] });
|
|
377
419
|
if (opts.format === 'sarif') emit(toSarif({ ...report, findings: [...report.findings, ...report.introduced] }, { toolVersion: VERSION }), opts);
|
|
@@ -393,7 +435,7 @@ try {
|
|
|
393
435
|
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); }
|
|
394
436
|
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); }
|
|
395
437
|
if (opts.head && !opts.base) { process.stderr.write('cobolwork: build --head needs --base\n'); process.exit(2); }
|
|
396
|
-
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 || [],
|
|
438
|
+
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 || [], allowedSigners: opts.allowedSigners || null, pdsExport: opts.pdsExport === true, precompile: opts.precompile === true });
|
|
397
439
|
stampRevisions(result.doc.summary, opts.head || null);
|
|
398
440
|
Object.assign(result.report.summary, { toolRevision: result.doc.summary.toolRevision, revision: result.doc.summary.revision });
|
|
399
441
|
Object.assign(result.provenance, { toolRevision: result.doc.summary.toolRevision, revision: result.doc.summary.revision });
|
|
@@ -436,12 +478,12 @@ try {
|
|
|
436
478
|
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); }
|
|
437
479
|
const report = scanAll(root, { repos, only: opts.only, advisoryFeeds: opts.advisoryFeeds, systemDirs });
|
|
438
480
|
const { entries, added } = baselineEntries(report.findings, held.entries, { action, reason: opts.reason, who: opts.who, expires: expires.toISOString(), at, rules: opts.rule || null });
|
|
439
|
-
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';
|
|
481
|
+
const written = JSON.stringify({ _comment: 'Judgements over cobolwork findings, matched by fingerprint. A suppression lapses at its expires date and the finding comes back.', version: BASELINE_VERSION, entries }, null, 1) + '\n';
|
|
440
482
|
writeFileSync(path, written);
|
|
441
483
|
recordInputs(journal, 0, root);
|
|
442
484
|
recordFindings(journal, report);
|
|
443
485
|
recordBaselineWrite(journal, { before: held.entries, after: entries, who: opts.who, expires: expires.toISOString(), reason: opts.reason, path, text: written });
|
|
444
|
-
const line = `${JSON.stringify({ tool: 'cobolwork-baseline', path, added, entries: entries.length })}\n`;
|
|
486
|
+
const line = `${JSON.stringify({ tool: 'cobolwork-baseline', schemaVersion: BASELINE_RESULT_SCHEMA_VERSION, path, added, entries: entries.length })}\n`;
|
|
445
487
|
recordOutput(journal, 'baseline', line);
|
|
446
488
|
process.stdout.write(line);
|
|
447
489
|
} else if (command === 'tui') {
|
|
@@ -475,7 +517,7 @@ try {
|
|
|
475
517
|
} else if (command === 'parse') {
|
|
476
518
|
const r = parseFile(root, { format: 'auto' });
|
|
477
519
|
emit({
|
|
478
|
-
tool: 'cobolwork-parse', file: r.file, format: r.format, finalFormat: r.finalFormat,
|
|
520
|
+
tool: 'cobolwork-parse', schemaVersion: PARSE_SCHEMA_VERSION, file: r.file, format: r.format, finalFormat: r.finalFormat,
|
|
479
521
|
programs: r.programs.map(p => ({ id: p.id, items: p.items.length, labels: p.labels.length, calls: p.calls.length, execs: p.execs.length, diagnostics: p.diags.length })),
|
|
480
522
|
copies: r.copies.map(c => ({ name: c.name, status: c.status })),
|
|
481
523
|
diagnostics: r.diags.length,
|
package/lib/baseline.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { resolvesInside } from './kernel/source-tree.mjs';
|
|
|
22
22
|
import { printable } from './kernel/printable.mjs';
|
|
23
23
|
|
|
24
24
|
export const BASELINE_FILE = 'cobolwork.baseline.json';
|
|
25
|
+
export const BASELINE_VERSION = 1;
|
|
25
26
|
export const SUPPRESSING = ['accept', 'false-positive', 'wont-fix', 'incorrect-scan-result'];
|
|
26
27
|
export const ACTIONS = [...SUPPRESSING, 'note', 'resolved'];
|
|
27
28
|
|
|
@@ -75,6 +76,14 @@ export function loadBaseline(root, { explicit = null, use = true, mayBeAbsent =
|
|
|
75
76
|
out.problems.push(`${path} is not readable as JSON: ${printable(err.message, 120)}`);
|
|
76
77
|
return out;
|
|
77
78
|
}
|
|
79
|
+
// A file with no version predates versioning and is read as version 0, which holds the same entries.
|
|
80
|
+
const version = raw && typeof raw === 'object' && raw.version !== undefined ? raw.version : 0;
|
|
81
|
+
if (!Number.isInteger(version) || version < 0) { out.problems.push(`${path} has version ${printable(JSON.stringify(version), 40)}, which is not a whole number from 0`); return out; }
|
|
82
|
+
if (version > BASELINE_VERSION) {
|
|
83
|
+
out.problems.push(`${path} is baseline version ${version}, and this cobolwork reads up to version ${BASELINE_VERSION}; upgrade cobolwork, so nothing was suppressed`);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
out.version = BASELINE_VERSION;
|
|
78
87
|
const entries = Array.isArray(raw?.entries) ? raw.entries : null;
|
|
79
88
|
if (!entries) { out.problems.push(`${path} has no entries array`); return out; }
|
|
80
89
|
entries.forEach((e, i) => {
|
package/lib/bms.mjs
CHANGED
|
@@ -56,11 +56,11 @@ const sublist = (v) => {
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
// L'NAME is NAME's length, and T', S', I', K', N', D' and O' its other attributes: the quote after the
|
|
59
|
-
// letter opens no string when the letter starts a term and a symbol
|
|
60
|
-
// are constants, and their quotes do open one.
|
|
59
|
+
// letter opens no string when the letter starts a term and a symbol, *, or a literal follows (L'*,
|
|
60
|
+
// L'=F'1'). CL8'TEXT' and D'1.5' are constants, and their quotes do open one.
|
|
61
61
|
// https://www.ibm.com/docs/en/hla-and-tf/1.6.0?topic=instructions-data-attributes
|
|
62
62
|
const attributeReference = (text, i) => /[LTSIKNDO]/i.test(text[i - 1] || '')
|
|
63
|
-
&& !/[A-Z0-9$#@_]/i.test(text[i - 2] || '') && /[A-Z$#@_
|
|
63
|
+
&& !/[A-Z0-9$#@_]/i.test(text[i - 2] || '') && /[A-Z$#@_&*=]/i.test(text[i + 1] || '');
|
|
64
64
|
|
|
65
65
|
// Folds physical lines into statements. A map continues a statement in two ways, often both in one
|
|
66
66
|
// file. An operand that fills the line to column 71 carries on in column 16 of the next line, which
|
|
@@ -70,12 +70,15 @@ const attributeReference = (text, i) => /[LTSIKNDO]/i.test(text[i - 1] || '')
|
|
|
70
70
|
// assembler, so it is reported rather than read.
|
|
71
71
|
// https://www.ibm.com/docs/en/SSLTBW_2.1.0/com.ibm.zos.v2r1.asma400/cl.htm
|
|
72
72
|
// https://www.ibm.com/docs/en/SSLTBW_2.1.0/com.ibm.zos.v2r1.asma400/altwmac.htm
|
|
73
|
-
|
|
73
|
+
// batch: read on past END, as HLASM's BATCH option assembles the next program, and keep JCL, SMP/E and
|
|
74
|
+
// IEBUPDTE control cards and an end-of-file mark, which no assembler statement starts with, as
|
|
75
|
+
// not-assembler lines.
|
|
76
|
+
export function foldStatements(src, { batch = false } = {}) {
|
|
74
77
|
const phys = String(src).replace(/\r\n?/g, '\n').split('\n');
|
|
75
78
|
const statements = [];
|
|
76
79
|
const diags = [];
|
|
77
80
|
let open = null; // { st, reading, quoted }: the statement the next line continues
|
|
78
|
-
let ended = false; //
|
|
81
|
+
let ended = false; // nothing after END is read, unless batch
|
|
79
82
|
|
|
80
83
|
const remark = (text, line, why) => {
|
|
81
84
|
if (/^[A-Z][A-Z0-9]*=/i.test(text)) diags.push({ sev: 'warn', line, text: `'${text}' ${why}, so the assembler reads it as a remark, not an operand` });
|
|
@@ -85,7 +88,8 @@ export function foldStatements(src) {
|
|
|
85
88
|
for (let i = from; i < text.length; i++) {
|
|
86
89
|
const c = text[i];
|
|
87
90
|
if (o.quoted) { o.st.field += c; if (c === "'") o.quoted = false; continue; }
|
|
88
|
-
|
|
91
|
+
// The letter of an attribute reference can end the line before: L in column 71, its quote in column 16.
|
|
92
|
+
if (c === "'" && attributeReference(o.st.field + text.slice(i), o.st.field.length)) { o.st.field += c; continue; }
|
|
89
93
|
if (c === "'") { o.quoted = true; o.st.field += c; continue; }
|
|
90
94
|
if (c === ' ' || c === '\t') {
|
|
91
95
|
o.reading = o.st.field.endsWith(',');
|
|
@@ -105,6 +109,7 @@ export function foldStatements(src) {
|
|
|
105
109
|
for (let i = 0; i < phys.length && !ended; i++) {
|
|
106
110
|
const line = i + 1;
|
|
107
111
|
const raw = phys[i];
|
|
112
|
+
let broke = null;
|
|
108
113
|
if (raw.length > 80) diags.push({ sev: 'warn', line, text: 'line is longer than 80 columns' });
|
|
109
114
|
const text = raw.slice(0, END_COLUMN);
|
|
110
115
|
const continues = raw.length > END_COLUMN && raw[END_COLUMN] !== ' ';
|
|
@@ -113,6 +118,7 @@ export function foldStatements(src) {
|
|
|
113
118
|
if (/\S/.test(text.slice(0, CONTINUE_COLUMN - 1))) {
|
|
114
119
|
// A comment banner that runs into column 72 is common and harmless; a statement is not.
|
|
115
120
|
if (open.st.kind === 'statement') diags.push({ sev: 'error', line, text: `a continuation must leave columns 1 to ${CONTINUE_COLUMN - 1} blank, so this line was read as a new statement` });
|
|
121
|
+
broke = open.st.kind;
|
|
116
122
|
finish(open);
|
|
117
123
|
open = null;
|
|
118
124
|
} else {
|
|
@@ -130,6 +136,10 @@ export function foldStatements(src) {
|
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
if (!text.trim()) continue;
|
|
139
|
+
if (batch && /^(?:\/\/|\/\*|\+\+|\.\/|\x1a)/.test(text)) {
|
|
140
|
+
statements.push({ kind: 'not-assembler', line, endLine: line, lines: [line], text });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
133
143
|
if (/^\*|^\.\*/.test(text)) {
|
|
134
144
|
const st = { kind: 'comment', line, endLine: line, lines: [line], text: text.slice(1) };
|
|
135
145
|
if (continues) open = { st, reading: false, quoted: false };
|
|
@@ -143,6 +153,10 @@ export function foldStatements(src) {
|
|
|
143
153
|
continue;
|
|
144
154
|
}
|
|
145
155
|
const st = { kind: 'statement', name: m[1] || null, operation: m[2].toUpperCase(), field: '', line, endLine: line, lines: [line] };
|
|
156
|
+
// The line before continued, so the assembler reads this one as part of it: as comment text
|
|
157
|
+
// after a comment, as an invalid continuation after a statement. It is read as a statement
|
|
158
|
+
// all the same, and marked.
|
|
159
|
+
if (broke) st.continues = broke;
|
|
146
160
|
const o = { st, reading: true, quoted: false };
|
|
147
161
|
let at = m[0].length;
|
|
148
162
|
while (text[at] === ' ' || text[at] === '\t') at++;
|
|
@@ -150,7 +164,7 @@ export function foldStatements(src) {
|
|
|
150
164
|
if (at < text.length) read(o, text, at, line);
|
|
151
165
|
if (continues) open = o;
|
|
152
166
|
else finish(o);
|
|
153
|
-
if (st.operation === 'END') ended =
|
|
167
|
+
if (st.operation === 'END') ended = !batch;
|
|
154
168
|
}
|
|
155
169
|
|
|
156
170
|
if (open) {
|