@portll/cobolwork 0.6.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.
Files changed (140) hide show
  1. package/README.md +49 -18
  2. package/STABILITY.md +76 -0
  3. package/bin/cobolwork.mjs +30 -7
  4. package/lib/bms.mjs +21 -7
  5. package/lib/build.mjs +108 -53
  6. package/lib/capabilities.mjs +9 -10
  7. package/lib/cics-commands.mjs +501 -9
  8. package/lib/compliance.mjs +11 -0
  9. package/lib/consequence.mjs +13 -0
  10. package/lib/control-workers.mjs +161 -0
  11. package/lib/control.mjs +52 -3
  12. package/lib/dataflow.mjs +176 -125
  13. package/lib/db2/cursor.mjs +15 -0
  14. package/lib/db2/read.mjs +170 -0
  15. package/lib/db2/rules.mjs +77 -0
  16. package/lib/db2/stmt/alter.mjs +473 -0
  17. package/lib/db2/stmt/grant.mjs +125 -0
  18. package/lib/db2/stmt/index.mjs +141 -0
  19. package/lib/db2/stmt/misc.mjs +325 -0
  20. package/lib/db2/stmt/routine.mjs +564 -0
  21. package/lib/db2/stmt/storage.mjs +146 -0
  22. package/lib/db2/stmt/table.mjs +540 -0
  23. package/lib/db2/stmt/view.mjs +146 -0
  24. package/lib/diff.mjs +17 -5
  25. package/lib/evidence/cli.mjs +14 -3
  26. package/lib/evidence/record.mjs +1 -1
  27. package/lib/evidence/store.mjs +44 -31
  28. package/lib/exec-reading.mjs +51 -0
  29. package/lib/execution.mjs +3 -2
  30. package/lib/explain.mjs +2 -0
  31. package/lib/exploitability.mjs +11 -2
  32. package/lib/hlasm/asm/data.mjs +13 -1
  33. package/lib/hlasm/asm/sections.mjs +3 -1
  34. package/lib/hlasm/instr.mjs +25 -0
  35. package/lib/hlasm/macro/authorization.mjs +24 -4
  36. package/lib/hlasm/macro/datasets.mjs +30 -17
  37. package/lib/hlasm/macro/io.mjs +68 -45
  38. package/lib/hlasm/macro/le.mjs +2 -2
  39. package/lib/hlasm/macro/operator.mjs +32 -21
  40. package/lib/hlasm/macro/program.mjs +126 -84
  41. package/lib/hlasm/macro/recovery.mjs +10 -5
  42. package/lib/hlasm/macro/storage.mjs +64 -14
  43. package/lib/hlasm/macro/structured.mjs +1 -1
  44. package/lib/hlasm/model.mjs +35 -10
  45. package/lib/hlasm/mvs38.mjs +47 -0
  46. package/lib/hlasm/operands.mjs +10 -0
  47. package/lib/hlasm/optable.mjs +61 -0
  48. package/lib/hlasm/read.mjs +22 -8
  49. package/lib/hlasm.mjs +44 -7
  50. package/lib/ims/dli.mjs +37 -0
  51. package/lib/ims/macro/dbd.mjs +299 -0
  52. package/lib/ims/macro/psb.mjs +286 -0
  53. package/lib/ims/model.mjs +149 -0
  54. package/lib/ims/operands.mjs +23 -0
  55. package/lib/ims/read.mjs +37 -0
  56. package/lib/ims/rules.mjs +135 -0
  57. package/lib/ironwork.mjs +17 -13
  58. package/lib/kernel/pds-archive.mjs +256 -0
  59. package/lib/kernel/registry.mjs +14 -8
  60. package/lib/kernel/shared-pass.mjs +34 -9
  61. package/lib/kernel/source-tree.mjs +104 -36
  62. package/lib/kernel/version-key.mjs +17 -0
  63. package/lib/layout.mjs +26 -31
  64. package/lib/parser.mjs +136 -17
  65. package/lib/pli/cursor.mjs +15 -0
  66. package/lib/pli/expr.mjs +101 -0
  67. package/lib/pli/include.mjs +82 -0
  68. package/lib/pli/layout.mjs +125 -0
  69. package/lib/pli/lex.mjs +198 -0
  70. package/lib/pli/program.mjs +280 -0
  71. package/lib/pli/rules/based.mjs +95 -0
  72. package/lib/pli/rules/conditions.mjs +68 -0
  73. package/lib/pli/rules/entry.mjs +130 -0
  74. package/lib/pli/rules/index.mjs +24 -0
  75. package/lib/pli/rules/preprocessor.mjs +55 -0
  76. package/lib/pli/statements.mjs +130 -0
  77. package/lib/pli/stmt/alloc.mjs +45 -0
  78. package/lib/pli/stmt/assignment.mjs +56 -0
  79. package/lib/pli/stmt/call.mjs +104 -0
  80. package/lib/pli/stmt/conditions.mjs +94 -0
  81. package/lib/pli/stmt/control.mjs +219 -0
  82. package/lib/pli/stmt/declare.mjs +149 -0
  83. package/lib/pli/stmt/exec.mjs +55 -0
  84. package/lib/pli/stmt/io.mjs +239 -0
  85. package/lib/pli/stmt/misc.mjs +4 -0
  86. package/lib/pli/stmt/preprocessor.mjs +242 -0
  87. package/lib/pli/stmt/procedure.mjs +258 -0
  88. package/lib/pli/stmt/stream.mjs +283 -0
  89. package/lib/pli/storage.mjs +129 -0
  90. package/lib/precompile-check.mjs +124 -0
  91. package/lib/precompile-cics.mjs +7 -3
  92. package/lib/reach.mjs +11 -2
  93. package/lib/revision.json +1 -1
  94. package/lib/sarif.mjs +41 -3
  95. package/lib/scan.mjs +7 -1
  96. package/lib/sets/abend.mjs +16 -6
  97. package/lib/sets/cics.mjs +15 -35
  98. package/lib/sets/compile.mjs +41 -15
  99. package/lib/sets/crypto.mjs +5 -3
  100. package/lib/sets/ddl.mjs +36 -0
  101. package/lib/sets/flow.mjs +18 -1
  102. package/lib/sets/hidden.mjs +5 -3
  103. package/lib/sets/hlasm.mjs +72 -12
  104. package/lib/sets/ims.mjs +139 -0
  105. package/lib/sets/log.mjs +6 -6
  106. package/lib/sets/opaque.mjs +27 -7
  107. package/lib/sets/pli.mjs +40 -0
  108. package/lib/sets/recon.mjs +5 -3
  109. package/lib/sets/secrets.mjs +5 -3
  110. package/lib/sets/semantics.mjs +3 -0
  111. package/lib/sets/web.mjs +36 -22
  112. package/lib/site.mjs +10 -0
  113. package/lib/sources.mjs +80 -20
  114. package/lib/statement-cursor.mjs +67 -0
  115. package/lib/verify.mjs +3 -2
  116. package/lib/version.mjs +6 -0
  117. package/package.json +3 -2
  118. package/rules/compliance-cobit2019.json +432 -1
  119. package/rules/compliance-dora.json +414 -1
  120. package/rules/compliance-ffiec.json +414 -1
  121. package/rules/compliance-nist80053.json +466 -1
  122. package/rules/hlasm-optables.json +8024 -0
  123. package/schema/cobolwork-baseline.schema.json +36 -0
  124. package/schema/cobolwork-build-provenance.schema.json +187 -0
  125. package/schema/cobolwork-build.schema.json +382 -0
  126. package/schema/cobolwork-capabilities.schema.json +239 -0
  127. package/schema/cobolwork-diff.schema.json +217 -0
  128. package/schema/cobolwork-evidence.schema.json +161 -0
  129. package/schema/cobolwork-execution.schema.json +53 -0
  130. package/schema/cobolwork-explain.schema.json +360 -0
  131. package/schema/cobolwork-finding.schema.json +465 -0
  132. package/schema/cobolwork-flow.schema.json +465 -0
  133. package/schema/cobolwork-gate.schema.json +211 -0
  134. package/schema/cobolwork-inventory.schema.json +206 -0
  135. package/schema/cobolwork-parse.schema.json +105 -0
  136. package/schema/cobolwork-reach.schema.json +74 -0
  137. package/schema/cobolwork-report.schema.json +559 -0
  138. package/schema/cobolwork-witness.schema.json +107 -0
  139. package/schema/cobolwork.baseline.schema.json +101 -0
  140. 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 |
@@ -64,6 +64,8 @@ request and writes SARIF for code scanning: [docs/github-action.md](docs/github-
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
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
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` |
69
71
 
@@ -133,7 +135,10 @@ language, are not in it.
133
135
  and the paragraph or section it sits in (the job, step and DD for JCL), and the flagged statement's
134
136
  own text. No line number goes into it, so code added above a finding does not change it. `diff`
135
137
  compares findings by it, and SARIF carries it as `partialFingerprints["cobolwork/v1"]`. Two findings
136
- 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.
137
142
 
138
143
  ### Findings and Claim Severity
139
144
 
@@ -223,7 +228,7 @@ entered ([docs/spec/evidence.md](docs/spec/evidence.md) §13.5).
223
228
  finding at the line it happened, with the input and the run's journal, once that journal verifies:
224
229
  `input-causes-abend-s0c7`, `-s0c4` (in a CICS task, the ASRA whose message names that check),
225
230
  `-subscript-range`, `input-causes-hang` for an input that keeps a loop running past the statement limit (S322), `input-selects-program`
226
- for an S806 whose journal shows the input reaching the CALL, or `input-causes-abend` for any other
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
227
232
  code ([docs/spec/evidence.md](docs/spec/evidence.md) §13.6). It reads manifests in format
228
233
  `ironwork-fuzz/v1`, and `ironwork-fuzz-interface/v1` from a subprogram fuzzed with generated
229
234
  arguments, whose findings say no caller run shows a caller passes them and warn by default; it
@@ -296,7 +301,10 @@ clean result.
296
301
  Some rules need facts no repository holds: which dataset qualifiers are production, which DDs reach
297
302
  the internal reader, the compiler options and runtime versions in use, which libraries are
298
303
  authorised. They go in `cobolwork.site.json`, and `node diag/propose-site.mjs <path>` drafts one from
299
- the estate's own JCL for a person to correct. Without a fact, the rule that needs it says it did not
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
300
308
  run, under `setsIncomplete`, rather than reporting a clean result. The benchmark tree has no site
301
309
  file, which is why three sets say so above. `advisoryCoverage` names the products the advisory rules
302
310
  searched, so a scan with no advisory finding says what that silence covers.
@@ -340,19 +348,19 @@ The parser is graded against GnuCOBOL's own listing (`cobc -t -Xref -ftsymbols`)
340
348
  data item with the size the compiler computed, every label, called programs, and which references
341
349
  write to a field. `diag/grade-against-gnucobol.mjs` runs that comparison over a corpus, on
342
350
  repositories never used while building the parser. The 100 and 300 sets were measured 2026-09-18,
343
- the 500 set 2026-09-26:
351
+ the 500 set 2026-10-04:
344
352
 
345
353
  | Corpus | Files the compiler accepted | Data items | Sizes | Labels and calls |
346
354
  |---|---|---|---|---|
347
355
  | 100 repositories, held out | 489 | 100% recall, 100% precision | 0 disagree of 15,888 | 100% |
348
356
  | 300 repositories, held out | 2,210 | 99.9% / 100% | 8 disagree of 94,786 | 100% / 99.7% |
349
- | 500 repositories, held out | 21,624 | 99.98% / 99.998% | 5,881 disagree of 733,835 | 100% / 100% |
357
+ | 500 repositories, held out | 21,707 | 100% / 100% | 0 disagree of 719,184 | 100% / 100% |
350
358
 
351
- On the 500 set, 5,643 of the 5,881 size disagreements come from one repository that vendors a COBOL
352
- research dataset; the other repositories disagree on 238 of 343,444. The grade covers only programs
353
- GnuCOBOL accepts, so a program with EXEC SQL or EXEC CICS is graded only through the precompiler
354
- stand-in in `diag/precompiler.mjs`, which rewrites what the parser would otherwise have to read. The
355
- 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`,
356
364
  so they run without GnuCOBOL.
357
365
 
358
366
  HLASM is graded against z390, an HLASM-compatible assembler, by `diag/hlasm-oracle.mjs` and
@@ -371,11 +379,29 @@ the reader follows: an EQU's length, where z390 gives 1 and the reference the le
371
379
  leftmost term; and anything after a literal pool where z390 pads a literal that the reference packs.
372
380
  A file with an error that can move a location is not graded either, such as an undefined symbol in
373
381
  a statement holding a literal, which z390 then leaves out of the pool. Of the statements outside conditional
374
- assembly, 99.5% parse on the dev corpus and 99.7% on the held-out one; the rest are counted by
382
+ assembly, 99.5% parse on the dev corpus and 99.8% on the held-out one; the rest are counted by
375
383
  kind. `bench/hlasm-locate/` holds small programs, one assembler feature each, with z390's answers
376
384
  recorded beside them, so the tests check the locator without z390.
377
385
 
378
- `bench/cases/` holds 137 CWE-labelled cases, each paired with a near-miss negative: the same shape
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
379
405
  with the flaw removed. `node bench/run.mjs` scores any scanner's findings against them, by rule and
380
406
  file, never by line, and `npm test` fails if any case scores differently from its declaration.
381
407
  `--validate` compiles every COBOL case with GnuCOBOL, assembles every HLASM case with z390 when
@@ -407,10 +433,10 @@ and to a COBIT 2019 practice by identifier alone:
407
433
 
408
434
  | File | Instrument | Rules mapped |
409
435
  |---|---|---|
410
- | `rules/compliance-dora.json` | Regulation (EU) 2022/2554 (DORA) | 207 |
411
- | `rules/compliance-ffiec.json` | FFIEC IT Examination Handbook | 205, and 2 recorded as unmapped |
412
- | `rules/compliance-nist80053.json` | NIST SP 800-53 Rev. 5.2.0 | 205, and 2 recorded as unmapped |
413
- | `rules/compliance-cobit2019.json` | COBIT 2019 (ISACA), identifiers only | 205, and 2 recorded as unmapped |
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 |
414
440
 
415
441
  A COBIT 2019 row names the objective or practice and gives this project's own rationale; no ISACA
416
442
  text is reproduced, so reading what a practice says needs a copy of the framework. The practice is
@@ -418,12 +444,17 @@ chosen at the NIST control, through a crosswalk in `diag/map-compliance.mjs`, so
418
444
  from the rule's NIST clause.
419
445
 
420
446
  `node diag/map-compliance.mjs` refuses to write a quote the cached instrument does not contain, and
421
- 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
422
448
  qualified assessor has reviewed. Where no control genuinely covers a rule, as for committing an LPAR
423
449
  name to a repository, the rule is recorded as unmapped with a reason rather than mapped to the
424
450
  nearest control that reads plausibly. What else the mapping does not claim is in
425
451
  [docs/rule-sets.md](docs/rule-sets.md#compliance).
426
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
+
427
458
  ## Tests
428
459
 
429
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,6 +14,7 @@ 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';
@@ -28,6 +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';
32
+ import { optableLevel } from '../lib/hlasm/optable.mjs';
31
33
 
32
34
  const STARTED = new Date().toISOString();
33
35
 
@@ -59,7 +61,7 @@ const USAGE = `cobolwork ${VERSION} — COBOL, JCL and CICS security analysis, n
59
61
  what this cobolwork can do: commands and options, the version of every
60
62
  document it writes, its fingerprint version, the file kinds it reads, and
61
63
  the commit it runs from
62
- 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>...]
63
65
  the build gate: every finding ranked LOW to KNOWN-EXPLOITABLE, the
64
66
  policy's blocking findings and compiler options checked, and the
65
67
  compiler run only on a pass. Exits 0 pass, 1 fail, 3 undecided, 4 the
@@ -91,6 +93,13 @@ Options
91
93
  --pds-export scan: the directory holds partitioned data sets' members as files, as
92
94
  zowe zos-files download all-members writes them (hlq/llq/member.txt) or in a
93
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
94
103
  --copylib <dir>[,<dir>] copy libraries the estate keeps outside the repository, searched after the
95
104
  tree's own copybooks, as COBCPY is; a copybook found there is read, not reported missing
96
105
  --report <file> tui, explain: read a stored scan report instead of scanning
@@ -113,6 +122,9 @@ Options
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
@@ -168,6 +180,10 @@ function parseArgs(argv) {
168
180
  else if (a === '--rule') opts.rule = list();
169
181
  else if (a === '--advisories') opts.advisoryFeeds = [...new Set(list().map(x => resolve(x)))];
170
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;
171
187
  else if (a === '--copylib') opts.copylib = [...new Set(list().map(x => resolve(x)))];
172
188
  else if (a === '--report') opts.report = value();
173
189
  else if (a === '--keys') opts.keys = value();
@@ -294,7 +310,14 @@ if (opts.copylib && opts._.length && !COPYLIB_COMMANDS.includes(opts._[0])) {
294
310
  const notDirs = (opts.copylib || []).filter((d) => { try { return !statSync(d).isDirectory(); } catch { return true; } });
295
311
  if (notDirs.length) { process.stderr.write(`cobolwork: --copylib ${notDirs.join(', ')} is not a directory\n`); process.exit(2); }
296
312
  const systemDirs = opts.copylib || [];
297
- if (opts.pdsExport && opts._.length && opts._[0] !== 'scan') { process.stderr.write('cobolwork: --pds-export is for scan only\n'); process.exit(2); }
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); }
298
321
  if (opts.pdsExport && opts.repos) { process.stderr.write('cobolwork: --pds-export reads one export; --repos does not apply\n'); process.exit(2); }
299
322
  if (opts.json && opts._.length && opts._[0] !== 'capabilities') {
300
323
  process.stderr.write(`cobolwork: --json is for capabilities; every other command writes JSON unless --format says otherwise\n`);
@@ -365,7 +388,7 @@ try {
365
388
  } else if (command === 'evidence') {
366
389
  process.exitCode = evidenceCommand(target, opts, { toolVersion: VERSION, write: (s) => process.stdout.write(s) });
367
390
  } else if (command === 'scan' || command === 'flow') {
368
- 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 };
369
392
  // The site file sits beside the members and is not one of them, so it is named rather than found.
370
393
  const site = resolve(root, SITE_FILE);
371
394
  const pds = opts.pdsExport ? pdsExportTree(root, { systemDirs }) : null;
@@ -390,7 +413,7 @@ try {
390
413
  warnCoverage(report);
391
414
  } else if (command === 'diff') {
392
415
  if (!opts.base) { process.stderr.write(`cobolwork: diff needs --base <ref>\n`); process.exit(2); }
393
- 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 });
394
417
  stampRevisions(report.summary, opts.head || null);
395
418
  recordFindings(journal, { findings: [...report.findings, ...(report.introduced || [])] });
396
419
  if (opts.format === 'sarif') emit(toSarif({ ...report, findings: [...report.findings, ...report.introduced] }, { toolVersion: VERSION }), opts);
@@ -412,7 +435,7 @@ try {
412
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); }
413
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); }
414
437
  if (opts.head && !opts.base) { process.stderr.write('cobolwork: build --head needs --base\n'); process.exit(2); }
415
- 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 });
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 });
416
439
  stampRevisions(result.doc.summary, opts.head || null);
417
440
  Object.assign(result.report.summary, { toolRevision: result.doc.summary.toolRevision, revision: result.doc.summary.revision });
418
441
  Object.assign(result.provenance, { toolRevision: result.doc.summary.toolRevision, revision: result.doc.summary.revision });
@@ -460,7 +483,7 @@ try {
460
483
  recordInputs(journal, 0, root);
461
484
  recordFindings(journal, report);
462
485
  recordBaselineWrite(journal, { before: held.entries, after: entries, who: opts.who, expires: expires.toISOString(), reason: opts.reason, path, text: written });
463
- 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`;
464
487
  recordOutput(journal, 'baseline', line);
465
488
  process.stdout.write(line);
466
489
  } else if (command === 'tui') {
@@ -494,7 +517,7 @@ try {
494
517
  } else if (command === 'parse') {
495
518
  const r = parseFile(root, { format: 'auto' });
496
519
  emit({
497
- 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,
498
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 })),
499
522
  copies: r.copies.map(c => ({ name: c.name, status: c.status })),
500
523
  diagnostics: r.diags.length,
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 follows. CL8'TEXT' and D'1.5'
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$#@_&]/i.test(text[i + 1] || '');
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
- export function foldStatements(src) {
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; // the assembler reads nothing after END
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
- if (c === "'" && attributeReference(text, i)) { o.st.field += c; continue; }
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 = true;
167
+ if (st.operation === 'END') ended = !batch;
154
168
  }
155
169
 
156
170
  if (open) {