@portll/cobolwork 0.4.0 → 0.6.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 (78) hide show
  1. package/README.md +53 -5
  2. package/bin/cobolwork.mjs +34 -9
  3. package/lib/baseline.mjs +9 -0
  4. package/lib/bms.mjs +8 -0
  5. package/lib/build.mjs +33 -8
  6. package/lib/capabilities.mjs +6 -3
  7. package/lib/consequence.mjs +17 -0
  8. package/lib/constants.mjs +60 -0
  9. package/lib/control-reuse.mjs +113 -0
  10. package/lib/control.mjs +86 -18
  11. package/lib/csd.mjs +6 -1
  12. package/lib/dataflow.mjs +343 -95
  13. package/lib/equivalence.mjs +44 -5
  14. package/lib/evidence/cli.mjs +13 -5
  15. package/lib/evidence/record.mjs +16 -6
  16. package/lib/evidence/seal.mjs +17 -0
  17. package/lib/evidence/store.mjs +30 -14
  18. package/lib/evidence/timestamp.mjs +83 -0
  19. package/lib/evidence/verify.mjs +120 -42
  20. package/lib/hlasm/asm/data.mjs +116 -0
  21. package/lib/hlasm/asm/listing.mjs +43 -0
  22. package/lib/hlasm/asm/output.mjs +45 -0
  23. package/lib/hlasm/asm/sections.mjs +159 -0
  24. package/lib/hlasm/asm/symbols.mjs +78 -0
  25. package/lib/hlasm/exec.mjs +27 -0
  26. package/lib/hlasm/expr.mjs +167 -0
  27. package/lib/hlasm/instr.mjs +48 -0
  28. package/lib/hlasm/locate.mjs +333 -0
  29. package/lib/hlasm/macro/authorization.mjs +126 -0
  30. package/lib/hlasm/macro/datasets.mjs +94 -0
  31. package/lib/hlasm/macro/io.mjs +139 -0
  32. package/lib/hlasm/macro/le.mjs +53 -0
  33. package/lib/hlasm/macro/linkage.mjs +143 -0
  34. package/lib/hlasm/macro/operator.mjs +66 -0
  35. package/lib/hlasm/macro/program.mjs +142 -0
  36. package/lib/hlasm/macro/recovery.mjs +80 -0
  37. package/lib/hlasm/macro/storage.mjs +117 -0
  38. package/lib/hlasm/macro/structured.mjs +131 -0
  39. package/lib/hlasm/model.mjs +72 -0
  40. package/lib/hlasm/operands.mjs +49 -0
  41. package/lib/hlasm/read.mjs +116 -0
  42. package/lib/hlasm.mjs +79 -0
  43. package/lib/inventory.mjs +10 -7
  44. package/lib/ironwork-ids.mjs +24 -0
  45. package/lib/ironwork.mjs +4 -1
  46. package/lib/kernel/registry.mjs +24 -17
  47. package/lib/kernel/shared-pass.mjs +138 -0
  48. package/lib/options.mjs +26 -4
  49. package/lib/policy.mjs +6 -0
  50. package/lib/precompile-cics.mjs +1 -1
  51. package/lib/revision.json +1 -1
  52. package/lib/sarif.mjs +3 -0
  53. package/lib/scan.mjs +21 -11
  54. package/lib/sets/abend.mjs +115 -13
  55. package/lib/sets/build.mjs +43 -37
  56. package/lib/sets/cics.mjs +5 -3
  57. package/lib/sets/compile.mjs +5 -3
  58. package/lib/sets/copybook.mjs +5 -3
  59. package/lib/sets/crypto.mjs +110 -0
  60. package/lib/sets/flow.mjs +32 -7
  61. package/lib/sets/hlasm.mjs +177 -0
  62. package/lib/sets/jcl.mjs +15 -1
  63. package/lib/sets/log.mjs +5 -3
  64. package/lib/sets/priv.mjs +5 -3
  65. package/lib/sets/secrets.mjs +110 -0
  66. package/lib/sets/semantics.mjs +5 -3
  67. package/lib/sets/web.mjs +104 -7
  68. package/lib/sources.mjs +12 -2
  69. package/package.json +1 -1
  70. package/rules/compliance-cobit2019.json +216 -0
  71. package/rules/compliance-dora.json +234 -0
  72. package/rules/compliance-ffiec.json +234 -0
  73. package/rules/compliance-nist80053.json +234 -0
  74. package/rules/gitleaks-mainframe.toml +61 -16
  75. package/rules/hlasm-instructions.json +2396 -0
  76. package/rules/hlasm-operations.json +451 -0
  77. package/rules/icsf-services.json +1178 -0
  78. package/schema/cobolwork.policy.schema.json +7 -0
package/lib/control.mjs CHANGED
@@ -622,10 +622,14 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
622
622
 
623
623
  // ---- checks ----
624
624
  // The storage an item lies in: its record, or the record an 01 REDEFINES lays itself over.
625
+ const recordOf = new Map();
625
626
  const itemsInRecord = (item) => {
626
- let top = item;
627
+ let top = recordOf.get(item);
628
+ if (top) return top;
629
+ top = item;
627
630
  while (top.parent) top = top.parent;
628
631
  for (let hops = 0; top.redefinesItem && hops < 64; hops++) top = top.redefinesItem;
632
+ if (item.parent) recordOf.set(item, top);
629
633
  return top;
630
634
  };
631
635
  const fieldKey = (f) => (f.index ? `IX:${f.index}` : f);
@@ -639,6 +643,33 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
639
643
  const fEnd = f.offset + (f.contributes || f.size);
640
644
  return w.offset < fEnd && f.offset < wEnd;
641
645
  };
646
+ // Entries grouped by the storage their item lies in, so a lookup tries only what `overlaps` can
647
+ // accept: the same record, the same index, or a 66-level item, which overlaps any field.
648
+ const byStorage = (entries, itemOf) => {
649
+ const byRecord = new Map();
650
+ const byIndex = new Map();
651
+ const renames = [];
652
+ const plain = [];
653
+ const add = (map, k, e) => { const list = map.get(k); if (list) list.push(e); else map.set(k, [e]); };
654
+ for (const e of entries) {
655
+ const it = itemOf(e);
656
+ if (it.index) { add(byIndex, it.index, e); continue; }
657
+ plain.push(e);
658
+ if (it.level === 66) renames.push(e);
659
+ else add(byRecord, itemsInRecord(it), e);
660
+ }
661
+ return {
662
+ mayOverlap: (it) => {
663
+ if (it.index) return byIndex.get(it.index) || [];
664
+ if (it.level === 66) return plain;
665
+ const same = byRecord.get(itemsInRecord(it));
666
+ return same ? (renames.length ? same.concat(renames) : same) : renames;
667
+ },
668
+ };
669
+ };
670
+ // Arguments the CICS API documents as data-values, which the command reads and never fills: the
671
+ // program or transaction it transfers to or starts, and the system it ships the request to.
672
+ const CICS_READS = { LINK: ['PROGRAM', 'TRANSID', 'SYSID'], XCTL: ['PROGRAM'], START: ['TRANSID', 'SYSID'], RETURN: ['TRANSID'], LOAD: ['PROGRAM'], RELEASE: ['PROGRAM'] };
642
673
  const writtenBy = (n) => {
643
674
  const out = [];
644
675
  const add = (tok) => { const f = tok ? resolve(tok) : null; if (f) out.push(f.level === 88 && f.parent ? f.parent : f); };
@@ -667,28 +698,31 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
667
698
  }
668
699
  }
669
700
  } else if (n.e) {
670
- // A command may fill what any option is given directly, except what it sends FROM. A name
671
- // inside that argument's own parentheses - a subscript, a reference modification - is read.
701
+ // A command may fill what any option is given directly, except what it sends FROM and the
702
+ // data-values it reads. A name inside that argument's own parentheses - a subscript, a
703
+ // reference modification - is read.
704
+ const reads = (n.e.kind === 'CICS' && CICS_READS[n.e.toks[0]?.u]) || [];
672
705
  let depth = 0;
673
706
  let option = null;
674
707
  for (let i = 0; i < n.e.toks.length; i++) {
675
708
  const t = n.e.toks[i];
676
709
  if (t.t === 'sep') { depth += t.v === '(' ? 1 : -1; continue; }
677
710
  if (depth === 0) { option = t.t === 'word' ? t.u : option; continue; }
678
- if (depth === 1 && t.t === 'word' && option !== 'FROM') add(t);
711
+ if (depth === 1 && t.t === 'word' && option !== 'FROM' && !reads.includes(option)) add(t);
679
712
  }
680
713
  }
681
714
  return out;
682
715
  };
683
716
  // A VALUE holds for the whole run only if no statement writes storage that overlaps the item.
684
- let writtenItems = null;
717
+ let written = null;
685
718
  const neverWritten = (it) => {
686
719
  if (it.external) return false;
687
- if (!writtenItems) {
688
- writtenItems = [];
689
- for (const n of nodes) if (n.kind === 'stmt' || n.kind === 'exec' || n.kind === 'step' || n.kind === 'call') writtenItems.push(...writtenBy(n));
720
+ if (!written) {
721
+ const all = [];
722
+ for (const n of nodes) if (n.kind === 'stmt' || n.kind === 'exec' || n.kind === 'step' || n.kind === 'call') all.push(...writtenBy(n));
723
+ written = byStorage(all, (w) => w);
690
724
  }
691
- return !writtenItems.some((w) => overlaps(w, it));
725
+ return !written.mayOverlap(it).some((w) => overlaps(w, it));
692
726
  };
693
727
  const constValue = (toks) => {
694
728
  if (toks.length === 3 && WORD(toks[0], 'LENGTH') && WORD(toks[1], 'OF')) {
@@ -2373,17 +2407,17 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
2373
2407
  for (const f of [c.x, c.t, c.f, c.rt, c.rf]) if (f != null) list.push(f);
2374
2408
  factsOfKey.set(c.key, list);
2375
2409
  }
2410
+ const checkedFields = byStorage([...factsOfKey.keys()].filter((key) => fields.get(key)), (key) => fields.get(key));
2376
2411
  const killCache = new Map();
2377
2412
  const killOf = (w) => {
2378
2413
  if (!w) return null;
2379
2414
  let bits = killCache.get(w);
2380
2415
  if (bits !== undefined) return bits;
2381
2416
  bits = null;
2382
- for (const [key, list] of factsOfKey) {
2383
- const f = fields.get(key);
2384
- if (!f || !overlaps(w, f)) continue;
2417
+ for (const key of checkedFields.mayOverlap(w)) {
2418
+ if (!overlaps(w, fields.get(key))) continue;
2385
2419
  bits ||= new Uint32Array(words);
2386
- for (const x of list) bits[x >>> 5] |= 1 << (x & 31);
2420
+ for (const x of factsOfKey.get(key)) bits[x >>> 5] |= 1 << (x & 31);
2387
2421
  }
2388
2422
  killCache.set(w, bits);
2389
2423
  return bits;
@@ -2629,10 +2663,15 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
2629
2663
  // Every route from `entry` that reaches `stopAt` or the program's end, meeting at joins. A call
2630
2664
  // whose range never returns ends the route, as a STOP RUN does. `calls` collects the ranges the
2631
2665
  // routes perform, for whoever needs to know what depends on what.
2666
+ // A run holds a bitset per node it reaches. Those of a run nobody reads any more are handed back
2667
+ // and reused, since allocating them afresh for every run was most of the analysis's collection time.
2668
+ const spare = [];
2669
+ const copyOf = (bits) => { const a = spare.pop(); if (!a) return bits.slice(); a.set(bits); return a; };
2670
+ const release = (IN) => { for (const a of IN.values()) spare.push(a); };
2632
2671
  function run(entry, input, stopAt, calls = null) {
2633
2672
  const IN = new Map();
2634
2673
  let exitOut = null;
2635
- IN.set(entry, input.slice());
2674
+ IN.set(entry, copyOf(input));
2636
2675
  push(entry);
2637
2676
  while (heap.length) {
2638
2677
  const id = pop();
@@ -2643,7 +2682,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
2643
2682
  if (id === stopAt) { if (exitOut) meetInto(exitOut, out); else exitOut = out.slice(); continue; }
2644
2683
  for (const s of n.succ) {
2645
2684
  const cur = IN.get(s);
2646
- if (!cur) { IN.set(s, out.slice()); push(s); } else if (meetInto(cur, out)) push(s);
2685
+ if (!cur) { IN.set(s, copyOf(out)); push(s); } else if (meetInto(cur, out)) push(s);
2647
2686
  }
2648
2687
  }
2649
2688
  return { IN, exitOut };
@@ -2675,7 +2714,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
2675
2714
  for (const d of n.derive || []) if (d.needs.every((x) => has(bits, x))) approved.add(d);
2676
2715
  for (const entry of n.implies || []) if (entry[2] && has(bits, entry[0])) approved.add(entry);
2677
2716
  }
2678
- const top = run(r.entry, ONES, r.end, calls).exitOut;
2717
+ release(fromNone.IN);
2718
+ const fromAll = run(r.entry, ONES, r.end, calls);
2719
+ const top = fromAll.exitOut;
2720
+ release(fromAll.IN);
2679
2721
  approved = null;
2680
2722
  for (const k of calls) { if (!callers.has(k)) callers.set(k, new Set()); callers.get(k).add(r); }
2681
2723
  const old = summaries.get(r.key);
@@ -2704,7 +2746,9 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
2704
2746
  waiting.delete(c.key);
2705
2747
  const input = c.stopAt == null ? ZERO : rangeIn.get(c.key);
2706
2748
  const res = run(c.entry, input, c.stopAt);
2749
+ const superseded = results.get(c.key);
2707
2750
  results.set(c.key, res.IN);
2751
+ if (superseded) release(superseded);
2708
2752
  for (const [id, bits] of res.IN) {
2709
2753
  const n = nodes[id];
2710
2754
  if (n.kind !== 'call' || !byKey.has(n.range.key)) continue;
@@ -2723,7 +2767,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
2723
2767
  for (const IN of results.values()) {
2724
2768
  for (const [id, bits] of IN) {
2725
2769
  const had = facts.get(id);
2726
- if (had) meetInto(had, bits); else facts.set(id, bits.slice());
2770
+ if (had) meetInto(had, bits); else facts.set(id, bits);
2727
2771
  }
2728
2772
  }
2729
2773
  };
@@ -2906,7 +2950,31 @@ function readersOf(tree) {
2906
2950
  return out;
2907
2951
  }
2908
2952
 
2909
- export const hasFact = (bits, x) => x != null && bits != null && (bits[x >>> 5] & (1 << (x & 31))) !== 0;
2953
+ // A fact set is a Uint32Array with one bit per fact, or, where a program keeps it after its analysis,
2954
+ // a Uint16Array holding the sorted numbers of the facts that hold (sparseFacts).
2955
+ export const hasFact = (bits, x) => x != null && bits != null
2956
+ && (bits instanceof Uint16Array ? holdsSorted(bits, x) : (bits[x >>> 5] & (1 << (x & 31))) !== 0);
2957
+ function holdsSorted(list, x) {
2958
+ let lo = 0;
2959
+ let hi = list.length - 1;
2960
+ while (lo <= hi) {
2961
+ const mid = (lo + hi) >>> 1;
2962
+ if (list[mid] === x) return true;
2963
+ if (list[mid] < x) lo = mid + 1; else hi = mid - 1;
2964
+ }
2965
+ return false;
2966
+ }
2967
+
2968
+ // The facts a set holds as their sorted numbers, where every number fits 16 bits; the set itself otherwise.
2969
+ export function sparseFacts(bits) {
2970
+ if (bits.length * 32 > 65536) return bits;
2971
+ let n = 0;
2972
+ for (let i = 0; i < bits.length; i++) for (let w = bits[i]; w; w &= w - 1) n++;
2973
+ const out = new Uint16Array(n);
2974
+ let k = 0;
2975
+ for (let i = 0; i < bits.length; i++) for (let w = bits[i]; w; w &= w - 1) out[k++] = i * 32 + (31 - Math.clz32(w & -w));
2976
+ return out;
2977
+ }
2910
2978
 
2911
2979
  // What a node's checks say at a point, given the facts that hold there: 2 when the value it holds is
2912
2980
  // safe for the sink, 1 when a check has run on every route to the point, 0 when neither. The check
package/lib/csd.mjs CHANGED
@@ -14,7 +14,7 @@ const COMMAND = /^\s*(DEFINE|ADD|ALTER|APPEND|COPY|DELETE|INITIALIZE|LIST|REMOVE
14
14
  // { tdqueues: Map(name -> { type, ddname, indirect, line }),
15
15
  // transactions: Map(name -> { program, cmdsec, ressec, line }),
16
16
  // tcpipservices: Map(name -> { port, protocol, ssl, authenticate, transaction, line }),
17
- // urimaps: Map(name -> { usage, path, program, transaction, tcpipservice, line }) }
17
+ // urimaps: Map(name -> { usage, path, program, transaction, tcpipservice, scheme, host, attls, line }) }
18
18
  export function parseCsd(text) {
19
19
  const tdqueues = new Map();
20
20
  const transactions = new Map();
@@ -64,6 +64,11 @@ export function parseCsd(text) {
64
64
  program: attrs.get('PROGRAM') || null,
65
65
  transaction: attrs.get('TRANSACTION') || null,
66
66
  tcpipservice: attrs.get('TCPIPSERVICE') || null,
67
+ // A client URIMAP's scheme decides whether WEB OPEN URIMAP(name) asks for TLS; ATTLS(AWARE)
68
+ // has AT-TLS secure an HTTP scheme outside CICS.
69
+ scheme: attrs.get('SCHEME') || null,
70
+ host: attrs.get('HOST') || null,
71
+ attls: attrs.get('ATTLS') || null,
67
72
  line: b.line,
68
73
  });
69
74
  } else if (kind === 'TDQUEUE') {