@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.
- package/README.md +49 -18
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +30 -7
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +108 -53
- package/lib/capabilities.mjs +9 -10
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +13 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +52 -3
- package/lib/dataflow.mjs +176 -125
- 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/evidence/cli.mjs +14 -3
- package/lib/evidence/record.mjs +1 -1
- package/lib/evidence/store.mjs +44 -31
- 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 +13 -1
- package/lib/hlasm/asm/sections.mjs +3 -1
- package/lib/hlasm/instr.mjs +25 -0
- package/lib/hlasm/macro/authorization.mjs +24 -4
- package/lib/hlasm/macro/datasets.mjs +30 -17
- package/lib/hlasm/macro/io.mjs +68 -45
- package/lib/hlasm/macro/le.mjs +2 -2
- package/lib/hlasm/macro/operator.mjs +32 -21
- package/lib/hlasm/macro/program.mjs +126 -84
- package/lib/hlasm/macro/recovery.mjs +10 -5
- package/lib/hlasm/macro/storage.mjs +64 -14
- package/lib/hlasm/macro/structured.mjs +1 -1
- package/lib/hlasm/model.mjs +35 -10
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +10 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +22 -8
- 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/ironwork.mjs +17 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +14 -8
- package/lib/kernel/shared-pass.mjs +34 -9
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- 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/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +7 -3
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +41 -3
- package/lib/scan.mjs +7 -1
- package/lib/sets/abend.mjs +16 -6
- package/lib/sets/cics.mjs +15 -35
- package/lib/sets/compile.mjs +41 -15
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +18 -1
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +72 -12
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +6 -6
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +5 -3
- package/lib/sets/semantics.mjs +3 -0
- package/lib/sets/web.mjs +36 -22
- 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 +432 -1
- package/rules/compliance-dora.json +414 -1
- package/rules/compliance-ffiec.json +414 -1
- package/rules/compliance-nist80053.json +466 -1
- 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.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
|
|
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.
|
|
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-
|
|
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,
|
|
357
|
+
| 500 repositories, held out | 21,707 | 100% / 100% | 0 disagree of 719,184 | 100% / 100% |
|
|
350
358
|
|
|
351
|
-
On the 500 set
|
|
352
|
-
|
|
353
|
-
GnuCOBOL accepts, so a program with EXEC SQL or EXEC CICS is graded only
|
|
354
|
-
stand-in in `diag/precompiler.mjs`, which rewrites what the parser would
|
|
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.
|
|
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
|
-
|
|
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) |
|
|
411
|
-
| `rules/compliance-ffiec.json` | FFIEC IT Examination Handbook |
|
|
412
|
-
| `rules/compliance-nist80053.json` | NIST SP 800-53 Rev. 5.2.0 |
|
|
413
|
-
| `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 |
|
|
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]
|
|
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
|
|
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) {
|