@portll/cobolwork 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +89 -26
  2. package/STABILITY.md +76 -0
  3. package/bin/cobolwork.mjs +56 -14
  4. package/lib/baseline.mjs +9 -0
  5. package/lib/bms.mjs +21 -7
  6. package/lib/build.mjs +133 -53
  7. package/lib/capabilities.mjs +13 -11
  8. package/lib/cics-commands.mjs +501 -9
  9. package/lib/compliance.mjs +11 -0
  10. package/lib/consequence.mjs +17 -0
  11. package/lib/control-reuse.mjs +113 -0
  12. package/lib/control-workers.mjs +161 -0
  13. package/lib/control.mjs +77 -4
  14. package/lib/dataflow.mjs +402 -152
  15. package/lib/db2/cursor.mjs +15 -0
  16. package/lib/db2/read.mjs +170 -0
  17. package/lib/db2/rules.mjs +77 -0
  18. package/lib/db2/stmt/alter.mjs +473 -0
  19. package/lib/db2/stmt/grant.mjs +125 -0
  20. package/lib/db2/stmt/index.mjs +141 -0
  21. package/lib/db2/stmt/misc.mjs +325 -0
  22. package/lib/db2/stmt/routine.mjs +564 -0
  23. package/lib/db2/stmt/storage.mjs +146 -0
  24. package/lib/db2/stmt/table.mjs +540 -0
  25. package/lib/db2/stmt/view.mjs +146 -0
  26. package/lib/diff.mjs +17 -5
  27. package/lib/equivalence.mjs +44 -5
  28. package/lib/evidence/cli.mjs +26 -7
  29. package/lib/evidence/record.mjs +16 -6
  30. package/lib/evidence/seal.mjs +17 -0
  31. package/lib/evidence/store.mjs +44 -15
  32. package/lib/evidence/timestamp.mjs +83 -0
  33. package/lib/evidence/verify.mjs +120 -42
  34. package/lib/exec-reading.mjs +51 -0
  35. package/lib/execution.mjs +3 -2
  36. package/lib/explain.mjs +2 -0
  37. package/lib/exploitability.mjs +11 -2
  38. package/lib/hlasm/asm/data.mjs +128 -0
  39. package/lib/hlasm/asm/listing.mjs +43 -0
  40. package/lib/hlasm/asm/output.mjs +45 -0
  41. package/lib/hlasm/asm/sections.mjs +161 -0
  42. package/lib/hlasm/asm/symbols.mjs +78 -0
  43. package/lib/hlasm/exec.mjs +27 -0
  44. package/lib/hlasm/expr.mjs +167 -0
  45. package/lib/hlasm/instr.mjs +73 -0
  46. package/lib/hlasm/locate.mjs +333 -0
  47. package/lib/hlasm/macro/authorization.mjs +146 -0
  48. package/lib/hlasm/macro/datasets.mjs +107 -0
  49. package/lib/hlasm/macro/io.mjs +162 -0
  50. package/lib/hlasm/macro/le.mjs +53 -0
  51. package/lib/hlasm/macro/linkage.mjs +143 -0
  52. package/lib/hlasm/macro/operator.mjs +77 -0
  53. package/lib/hlasm/macro/program.mjs +184 -0
  54. package/lib/hlasm/macro/recovery.mjs +85 -0
  55. package/lib/hlasm/macro/storage.mjs +167 -0
  56. package/lib/hlasm/macro/structured.mjs +131 -0
  57. package/lib/hlasm/model.mjs +97 -0
  58. package/lib/hlasm/mvs38.mjs +47 -0
  59. package/lib/hlasm/operands.mjs +59 -0
  60. package/lib/hlasm/optable.mjs +61 -0
  61. package/lib/hlasm/read.mjs +130 -0
  62. package/lib/hlasm.mjs +44 -7
  63. package/lib/ims/dli.mjs +37 -0
  64. package/lib/ims/macro/dbd.mjs +299 -0
  65. package/lib/ims/macro/psb.mjs +286 -0
  66. package/lib/ims/model.mjs +149 -0
  67. package/lib/ims/operands.mjs +23 -0
  68. package/lib/ims/read.mjs +37 -0
  69. package/lib/ims/rules.mjs +135 -0
  70. package/lib/inventory.mjs +10 -7
  71. package/lib/ironwork-ids.mjs +24 -0
  72. package/lib/ironwork.mjs +20 -13
  73. package/lib/kernel/pds-archive.mjs +256 -0
  74. package/lib/kernel/registry.mjs +32 -23
  75. package/lib/kernel/shared-pass.mjs +163 -0
  76. package/lib/kernel/source-tree.mjs +104 -36
  77. package/lib/kernel/version-key.mjs +17 -0
  78. package/lib/layout.mjs +26 -31
  79. package/lib/options.mjs +26 -4
  80. package/lib/parser.mjs +136 -17
  81. package/lib/pli/cursor.mjs +15 -0
  82. package/lib/pli/expr.mjs +101 -0
  83. package/lib/pli/include.mjs +82 -0
  84. package/lib/pli/layout.mjs +125 -0
  85. package/lib/pli/lex.mjs +198 -0
  86. package/lib/pli/program.mjs +280 -0
  87. package/lib/pli/rules/based.mjs +95 -0
  88. package/lib/pli/rules/conditions.mjs +68 -0
  89. package/lib/pli/rules/entry.mjs +130 -0
  90. package/lib/pli/rules/index.mjs +24 -0
  91. package/lib/pli/rules/preprocessor.mjs +55 -0
  92. package/lib/pli/statements.mjs +130 -0
  93. package/lib/pli/stmt/alloc.mjs +45 -0
  94. package/lib/pli/stmt/assignment.mjs +56 -0
  95. package/lib/pli/stmt/call.mjs +104 -0
  96. package/lib/pli/stmt/conditions.mjs +94 -0
  97. package/lib/pli/stmt/control.mjs +219 -0
  98. package/lib/pli/stmt/declare.mjs +149 -0
  99. package/lib/pli/stmt/exec.mjs +55 -0
  100. package/lib/pli/stmt/io.mjs +239 -0
  101. package/lib/pli/stmt/misc.mjs +4 -0
  102. package/lib/pli/stmt/preprocessor.mjs +242 -0
  103. package/lib/pli/stmt/procedure.mjs +258 -0
  104. package/lib/pli/stmt/stream.mjs +283 -0
  105. package/lib/pli/storage.mjs +129 -0
  106. package/lib/policy.mjs +6 -0
  107. package/lib/precompile-check.mjs +124 -0
  108. package/lib/precompile-cics.mjs +8 -4
  109. package/lib/reach.mjs +11 -2
  110. package/lib/revision.json +1 -1
  111. package/lib/sarif.mjs +44 -3
  112. package/lib/scan.mjs +28 -12
  113. package/lib/sets/abend.mjs +82 -8
  114. package/lib/sets/build.mjs +43 -37
  115. package/lib/sets/cics.mjs +20 -38
  116. package/lib/sets/compile.mjs +46 -18
  117. package/lib/sets/copybook.mjs +5 -3
  118. package/lib/sets/crypto.mjs +5 -3
  119. package/lib/sets/ddl.mjs +36 -0
  120. package/lib/sets/flow.mjs +32 -8
  121. package/lib/sets/hidden.mjs +5 -3
  122. package/lib/sets/hlasm.mjs +124 -8
  123. package/lib/sets/ims.mjs +139 -0
  124. package/lib/sets/log.mjs +11 -9
  125. package/lib/sets/opaque.mjs +27 -7
  126. package/lib/sets/pli.mjs +40 -0
  127. package/lib/sets/priv.mjs +5 -3
  128. package/lib/sets/recon.mjs +5 -3
  129. package/lib/sets/secrets.mjs +112 -0
  130. package/lib/sets/semantics.mjs +8 -3
  131. package/lib/sets/web.mjs +39 -23
  132. package/lib/site.mjs +10 -0
  133. package/lib/sources.mjs +80 -20
  134. package/lib/statement-cursor.mjs +67 -0
  135. package/lib/verify.mjs +3 -2
  136. package/lib/version.mjs +6 -0
  137. package/package.json +3 -2
  138. package/rules/compliance-cobit2019.json +520 -5
  139. package/rules/compliance-dora.json +509 -5
  140. package/rules/compliance-ffiec.json +505 -1
  141. package/rules/compliance-nist80053.json +557 -1
  142. package/rules/gitleaks-mainframe.toml +46 -15
  143. package/rules/hlasm-instructions.json +2396 -0
  144. package/rules/hlasm-optables.json +8024 -0
  145. package/schema/cobolwork-baseline.schema.json +36 -0
  146. package/schema/cobolwork-build-provenance.schema.json +187 -0
  147. package/schema/cobolwork-build.schema.json +382 -0
  148. package/schema/cobolwork-capabilities.schema.json +239 -0
  149. package/schema/cobolwork-diff.schema.json +217 -0
  150. package/schema/cobolwork-evidence.schema.json +161 -0
  151. package/schema/cobolwork-execution.schema.json +53 -0
  152. package/schema/cobolwork-explain.schema.json +360 -0
  153. package/schema/cobolwork-finding.schema.json +465 -0
  154. package/schema/cobolwork-flow.schema.json +465 -0
  155. package/schema/cobolwork-gate.schema.json +211 -0
  156. package/schema/cobolwork-inventory.schema.json +206 -0
  157. package/schema/cobolwork-parse.schema.json +105 -0
  158. package/schema/cobolwork-reach.schema.json +74 -0
  159. package/schema/cobolwork-report.schema.json +559 -0
  160. package/schema/cobolwork-witness.schema.json +107 -0
  161. package/schema/cobolwork.baseline.schema.json +101 -0
  162. package/schema/cobolwork.policy.schema.json +7 -0
  163. package/schema/cobolwork.site.schema.json +116 -0
@@ -0,0 +1,146 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Db2 for z/OS CREATE VIEW, after the Db2 13 SQL Reference: the view's name and columns, its common
3
+ // table expressions and fullselect kept as tokens, the check option, and the names its FROM clauses read.
4
+
5
+ import { Db2Syntax } from '../cursor.mjs';
6
+ import { readName } from './table.mjs';
7
+
8
+ const isName = (t) => !!t && (t.t === 'word' || t.t === 'ident');
9
+ const isWord = (t, u) => !!t && t.t === 'word' && (Array.isArray(u) ? u.includes(t.u) : t.u === u);
10
+ const isOpen = (t) => !!t && t.t === 'op' && t.v === '(';
11
+ const isClose = (t) => !!t && t.t === 'op' && t.v === ')';
12
+
13
+ // What another dialect's view looks like at the token where a Db2 for z/OS one stops.
14
+ function hint(t, after) {
15
+ if (!t) return '';
16
+ if (t.t === 'op' && t.v === '/') return " ('/' qualifies names in Db2 for i system naming, not Db2 for z/OS)";
17
+ if (t.t !== 'word') return '';
18
+ if (t.u === 'IF' && (isWord(after, 'NOT') || isWord(after, 'EXISTS'))) return ' (IF [NOT] EXISTS is not Db2 for z/OS syntax)';
19
+ if (t.u === 'MATERIALIZED') return ' (Db2 for z/OS has materialized query tables, made by CREATE TABLE, not materialized views)';
20
+ if (t.u === 'READ' && isWord(after, 'ONLY')) return ' (WITH READ ONLY is Oracle, not Db2 for z/OS)';
21
+ if (t.u === 'CONSTRAINT') return ' (a named check option is Oracle, not Db2 for z/OS)';
22
+ if (t.u === 'WITH' && isOpen(after)) return ' (view options in parentheses are not Db2 for z/OS syntax)';
23
+ return '';
24
+ }
25
+
26
+ function refuse(c, what) {
27
+ const t = c.peek();
28
+ throw new Db2Syntax(`expected ${what}${t ? ` at '${t.v ?? t.t}'` : ' at end of statement'}${hint(t, c.peek(1))}`, t || c.peek(-1));
29
+ }
30
+
31
+ function closing(toks, i) {
32
+ let depth = 0;
33
+ for (let k = i; k < toks.length; k++) {
34
+ if (isOpen(toks[k])) depth++;
35
+ else if (isClose(toks[k]) && --depth === 0) return k;
36
+ }
37
+ return toks.length;
38
+ }
39
+
40
+ const atTop = (toks, words) => {
41
+ let depth = 0;
42
+ return toks.some((t) => {
43
+ if (isOpen(t)) depth++;
44
+ else if (isClose(t)) depth--;
45
+ return depth === 0 && isWord(t, words);
46
+ });
47
+ };
48
+
49
+ // The words that end a FROM clause; FOR does too unless a period specification follows the table.
50
+ const FROM_END = ['WHERE', 'GROUP', 'HAVING', 'ORDER', 'FETCH', 'OFFSET', 'LIMIT', 'UNION', 'EXCEPT', 'INTERSECT', 'WITH', 'OPTIMIZE', 'QUERYNO', 'SKIP'];
51
+
52
+ // One FROM clause from i: a name that starts a table reference is recorded; TABLE (…), XMLTABLE (…) and
53
+ // the like are read only for the subqueries inside them. Returns the index where the clause ends.
54
+ function fromClause(toks, i, out) {
55
+ let start = true;
56
+ for (; i < toks.length; i++) {
57
+ const t = toks[i];
58
+ if (isWord(t, FROM_END) || (isWord(t, 'FOR') && !isWord(toks[i + 1], ['SYSTEM_TIME', 'BUSINESS_TIME']))) return i;
59
+ if ((t.t === 'op' && t.v === ',') || isWord(t, 'JOIN')) { start = true; continue; }
60
+ if (isOpen(t)) {
61
+ const j = closing(toks, i);
62
+ const inner = toks.slice(i + 1, j);
63
+ if (start && !atTop(inner, ['SELECT', 'UNION', 'EXCEPT', 'INTERSECT'])) fromClause(inner, 0, out);
64
+ else fromNames(inner, out);
65
+ i = j;
66
+ start = false;
67
+ continue;
68
+ }
69
+ if (!start) continue;
70
+ start = false;
71
+ if (!isName(t) || isWord(t, ['TABLE', 'FINAL', 'OLD']) || isOpen(toks[i + 1])) continue;
72
+ const parts = [t.v];
73
+ while (toks[i + 1] && toks[i + 1].t === 'op' && toks[i + 1].v === '.' && isName(toks[i + 2])) { parts.push(toks[i + 2].v); i += 2; }
74
+ out.push(parts.join('.'));
75
+ }
76
+ return i;
77
+ }
78
+
79
+ // The names in the FROM clause of every subselect in toks, subqueries included; a FROM that no SELECT
80
+ // at its own depth precedes (EXTRACT (YEAR FROM D), IS DISTINCT FROM) is not one.
81
+ function fromNames(toks, out) {
82
+ let select = false;
83
+ for (let i = 0; i < toks.length; i++) {
84
+ const t = toks[i];
85
+ if (isOpen(t)) {
86
+ const j = closing(toks, i);
87
+ fromNames(toks.slice(i + 1, j), out);
88
+ i = j;
89
+ } else if (isWord(t, 'SELECT')) select = true;
90
+ else if (isWord(t, 'FROM') && select && !isWord(toks[i - 1], 'DISTINCT')) i = fromClause(toks, i + 1, out) - 1;
91
+ }
92
+ }
93
+
94
+ function readColumnNames(c) {
95
+ return c.items().map((item) => {
96
+ if (item.length !== 1 || !isName(item[0])) throw new Db2Syntax(`expected an unqualified column name${item[0] ? ` at '${item[0].v}'` : ''}`, item[0] || c.peek(-1));
97
+ return item[0].v;
98
+ });
99
+ }
100
+
101
+ function readCommonTableExpression(c) {
102
+ const t = c.next();
103
+ if (!isName(t)) { c.pos -= t ? 1 : 0; refuse(c, 'a common table expression name'); }
104
+ const cte = { name: t.v, columns: c.isOp('(') ? readColumnNames(c) : null, fullselect: null };
105
+ c.expectWord('AS');
106
+ if (!c.isOp('(')) refuse(c, "'(' and the common table expression's fullselect");
107
+ cte.fullselect = c.group();
108
+ return cte;
109
+ }
110
+
111
+ function readView(c) {
112
+ c.expectWord('CREATE');
113
+ if (c.isWord('OR') && c.isWord('REPLACE', 1)) refuse(c, 'VIEW (Db2 for z/OS has no CREATE OR REPLACE VIEW)');
114
+ if (!c.isWord('VIEW')) refuse(c, 'VIEW');
115
+ c.next();
116
+ if (c.isWord('IF') && c.isWord(['NOT', 'EXISTS'], 1)) refuse(c, 'a view name');
117
+ const parts = readName(c);
118
+ if (parts.length > 3) refuse(c, 'a view name of at most three parts');
119
+ const node = { kind: 'CREATE VIEW', name: parts.join('.'), columns: null, with: [], fullselect: null, checkOption: null, tables: [] };
120
+ if (c.isOp('(')) node.columns = readColumnNames(c);
121
+ if (!c.word('AS')) refuse(c, node.columns ? 'AS' : "'(' or AS");
122
+ if (c.word('WITH')) {
123
+ if (c.isWord('RECURSIVE')) refuse(c, 'a common table expression (WITH RECURSIVE is not Db2 for z/OS syntax)');
124
+ do node.with.push(readCommonTableExpression(c)); while (c.op(','));
125
+ }
126
+ if (!c.isWord('SELECT') && !c.isOp('(')) refuse(c, 'a fullselect');
127
+ node.fullselect = c.until((t) => isWord(t, 'WITH'));
128
+ if (c.word('WITH')) {
129
+ const how = c.word(['CASCADED', 'LOCAL']);
130
+ if (!c.isWord('CHECK')) refuse(c, how ? 'CHECK OPTION' : 'CASCADED, LOCAL or CHECK OPTION');
131
+ c.next();
132
+ c.expectWord('OPTION');
133
+ node.checkOption = how ? how.u : 'CASCADED';
134
+ if (!c.done()) refuse(c, 'the end of the statement after CHECK OPTION');
135
+ }
136
+ const names = [];
137
+ for (const cte of node.with) fromNames(cte.fullselect, names);
138
+ fromNames(node.fullselect, names);
139
+ const local = new Set(node.with.map((cte) => cte.name.toUpperCase()));
140
+ node.tables = [...new Set(names)].filter((n) => n.includes('.') || !local.has(n.toUpperCase()));
141
+ return node;
142
+ }
143
+
144
+ export const parsers = {
145
+ 'CREATE VIEW': (c) => readView(c),
146
+ };
package/lib/diff.mjs CHANGED
@@ -9,7 +9,7 @@ import { FLOW_MODEL, SCHEMA_VERSION, TOOL_VERSION } from './version.mjs';
9
9
  // A diff finding is located by program rather than by line, so it orders on its own key. The text
10
10
  // comparison underneath it is the shared one.
11
11
  import { byText, evidenceMap } from './kernel/findings.mjs';
12
- import { directoryTree, gitTree } from './kernel/source-tree.mjs';
12
+ import { directoryTree, gitTree, pdsExportRevision, pdsExportTree } from './kernel/source-tree.mjs';
13
13
  import { printable } from './kernel/printable.mjs';
14
14
  import { loadSite, SITE_FILE } from './site.mjs';
15
15
  import { loadBaseline, BASELINE_FILE, SUPPRESSING } from './baseline.mjs';
@@ -258,6 +258,7 @@ function siteChange(baseAt, headAt) {
258
258
  if (b.allowUnvalidatedPacks !== h.allowUnvalidatedPacks) parts.push(`allowUnvalidatedPacks ${h.allowUnvalidatedPacks}`);
259
259
  if (JSON.stringify(b.runtimeVersions) !== JSON.stringify(h.runtimeVersions)) parts.push('runtimeVersions');
260
260
  if (h.problems.length && h.problems.join() !== b.problems.join()) parts.push(`now reports: ${h.problems[0]}`);
261
+ if (h.warnings.length && h.warnings.join() !== b.warnings.join()) parts.push(`now warns: ${h.warnings[0]}`);
261
262
  return parts.length ? { file: SITE_FILE, change: printable(parts.join('; '), 400) } : null;
262
263
  }
263
264
 
@@ -307,14 +308,25 @@ export function withRefs(repo, baseRef, headRef, opts, fn) {
307
308
  }
308
309
  }
309
310
 
311
+ // The members of a PDS export's working tree whose files git tracks or would track: an archive's
312
+ // members by the archive's file.
313
+ function exportScope(repo, tree) {
314
+ const scope = gitScope(repo);
315
+ return new Set(tree.list().filter((p) => scope.has(resolve(repo, tree.origin(p).replace(/\([^)]*\)$/, '')))));
316
+ }
317
+
310
318
  // Reads each revision out of git without writing it anywhere; the head is the working tree when
311
- // headRef is null.
319
+ // headRef is null. With pdsExport each side is read as a PDS export, members named for their data
320
+ // sets on both.
312
321
  export function diffRefs(repo, baseRef, headRef = null, opts = {}) {
313
322
  const trees = { systemDirs: opts.systemDirs || [] };
314
- const base = gitTree(repo, baseRef, trees);
315
- const head = headRef ? gitTree(repo, headRef, trees) : repo;
316
- const res = diffTrees(base, head, headRef ? opts : { ...opts, allow: gitScope(repo) });
323
+ const revision = (ref) => (opts.pdsExport ? pdsExportRevision(repo, ref, trees) : gitTree(repo, ref, trees));
324
+ const base = revision(baseRef);
325
+ const head = headRef ? revision(headRef) : opts.pdsExport ? pdsExportTree(repo, trees) : repo;
326
+ const allow = headRef ? null : opts.pdsExport ? exportScope(repo, head) : gitScope(repo);
327
+ const res = diffTrees(base, head, allow ? { ...opts, allow } : opts);
317
328
  res.summary.base = baseRef;
318
329
  res.summary.head = headRef || WORKING_TREE;
330
+ if (opts.pdsExport) res.summary.pdsExport = { base: base.export, head: head.export };
319
331
  return res;
320
332
  }
@@ -50,6 +50,34 @@ function closure(root, file, idx, systemDirs) {
50
50
  return out;
51
51
  }
52
52
 
53
+ // The first program's paragraphs, by name, each as its own statements read with every position
54
+ // left out, so a paragraph that only moved is the same paragraph (as ironwork compare reads them).
55
+ function paragraphPrints(file) {
56
+ let program;
57
+ try { program = parseSource(readSource(file).text, file, { format: 'auto', mainDir: dirname(file) }).programs[0]; } catch { return null; }
58
+ if (!program) return null;
59
+ const positionFree = (x) => (Array.isArray(x) ? x.map(positionFree)
60
+ : x && typeof x === 'object' ? Object.fromEntries(Object.entries(x).filter(([k]) => !['line', 'file', 'at', 'end', 'col', 'fmt'].includes(k)).map(([k, v]) => [k, positionFree(v)]))
61
+ : x);
62
+ const labels = [...program.labels].sort((a, b) => a.at - b.at);
63
+ const prints = new Map();
64
+ labels.forEach((label, i) => {
65
+ if (label.kind !== 'P') return;
66
+ const end = i + 1 < labels.length ? labels[i + 1].at : Infinity;
67
+ const body = program.statements.filter((s) => s.at > label.at && s.at < end).map(positionFree);
68
+ prints.set(label.name, [...(prints.get(label.name) || []), JSON.stringify(body)]);
69
+ });
70
+ return prints;
71
+ }
72
+
73
+ // The head's paragraphs whose own statements the change edits or adds.
74
+ function editedParagraphs(baseFile, headFile) {
75
+ const before = paragraphPrints(baseFile);
76
+ const after = paragraphPrints(headFile);
77
+ if (!before || !after) return [];
78
+ return [...after].filter(([name, prints]) => String(before.get(name)) !== String(prints)).map(([name]) => name).sort();
79
+ }
80
+
53
81
  // Programs the change edits, adds or deletes, or whose copybooks it edits: [{ path, base, head, via }]
54
82
  // with each side's source digest (head null for a deleted program) and the copybooks that changed
55
83
  // under an unchanged program.
@@ -112,14 +140,21 @@ export function checkEquivalence({ baseDir, headDir, allow, files = [], allowed
112
140
  problems.push(`${s.file} names head ${h ? h.slice(0, 12) : 'nothing'} and base ${b ? b.slice(0, 12) : 'nothing'}, which match no program this change edits`);
113
141
  }
114
142
  }
143
+ const authorship = mode === 'machineAuthored' && repo && base ? machineAuthored(repo, base, head || 'HEAD', env) : null;
144
+ const required = mode === 'always' ? true : mode === 'machineAuthored' ? (authorship && authorship.known ? authorship.commits.length > 0 : null) : false;
115
145
  // Every statement that names a program is judged, and the program passes only if each does.
146
+ // Where equivalence may be required, a statement no allowed signer signed could have been written
147
+ // by anyone who can commit.
116
148
  const programs = changed.map((c) => {
117
149
  if (c.deleted) return { path: c.path, deleted: true, statement: null, ok: false, because: 'the change deletes this program, and no equivalence statement can show what its callers do now' };
118
150
  const matching = statements.filter((x) => x.statement && subjectDigest(x.statement, 'head') === c.head && subjectDigest(x.statement, 'base') === c.base);
119
151
  const via = c.via ? { via: c.via } : {};
120
152
  if (!matching.length) return { path: c.path, ...via, statement: null, ok: false, because: c.via ? `no equivalence statement names this program, whose copybook${c.via.length > 1 ? 's' : ''} ${c.via.join(', ')} the change edits` : 'no equivalence statement names this change' };
121
153
  const judged = matching.map((s) => judge(c, s));
122
- return judged.find((j) => !j.ok) || judged[0];
154
+ const failed = judged.find((j) => !j.ok);
155
+ if (failed) return failed;
156
+ if (!allowed && required !== false) return { ...judged[0], ok: false, because: 'no --allowed-signers were given, so nothing shows who wrote the statement' };
157
+ return judged[0];
123
158
  });
124
159
  function judge(c, s) {
125
160
  const via = c.via ? { via: c.via } : {};
@@ -128,15 +163,19 @@ export function checkEquivalence({ baseDir, headDir, allow, files = [], allowed
128
163
  const covered = !!p.coverage && typeof p.coverage === 'object' && Array.isArray(p.coverage.unreached) && p.coverage.unreached.every((u) => typeof u === 'string');
129
164
  const ranOn = new Set(((p.closure && p.closure.head) || []).map((x) => x.sha256));
130
165
  const missing = (c.viaDigests || []).filter((d) => !ranOn.has(d));
166
+ // ironwork decides which paragraphs changed; a paragraph whose statements this diff shows
167
+ // edited and the statement does not list as changed was never held to coverage.
168
+ const claimed = covered && Array.isArray(p.coverage.changed) ? new Set(p.coverage.changed.map((n) => String(n).toUpperCase())) : null;
169
+ const edited = covered && c.base ? editedParagraphs(join(baseDir, c.path), join(headDir, c.path)) : [];
170
+ const unclaimed = edited.filter((n) => !claimed || !claimed.has(n));
131
171
  const because = !PASSING.has(verdict) ? `the statement's verdict is ${verdict}${Array.isArray(p.inconclusive) && p.inconclusive.length ? ` (${p.inconclusive.join('; ')})` : ''}`
132
172
  : missing.length ? `the statement does not show the head run read the edited copybook${c.via.length > 1 ? 's' : ''} ${c.via.join(', ')}`
133
173
  : !covered ? 'the statement measured no coverage of the changed paragraphs, so it is inconclusive'
134
- : allowed && !s.signed ? `the statement is ${s.reason}`
135
- : null;
174
+ : unclaimed.length ? `the statement's coverage does not list ${unclaimed.join(', ')} as changed, and the change edits ${unclaimed.length > 1 ? 'them' : 'it'}`
175
+ : allowed && !s.signed ? `the statement is ${s.reason}`
176
+ : null;
136
177
  const unreached = covered ? p.coverage.unreached : [];
137
178
  return { path: c.path, ...via, statement: s.file, verdict, signed: s.signed, signer: s.signer, coverage: covered, ...(unreached.length ? { unreached } : {}), ok: because === null && !unreached.length, ...(because || unreached.length ? { because: because || `the inputs never reached ${unreached.join(', ')}` } : {}) };
138
179
  }
139
- const authorship = mode === 'machineAuthored' && repo && base ? machineAuthored(repo, base, head || 'HEAD', env) : null;
140
- const required = mode === 'always' ? true : mode === 'machineAuthored' ? (authorship && authorship.known ? authorship.commits.length > 0 : null) : false;
141
180
  return { mode, required, ...(authorship ? { machineAuthored: authorship } : {}), programs, problems };
142
181
  }
@@ -1,15 +1,26 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // `cobolwork evidence verify|seal|anchor`: the command half of docs/spec/evidence.md §7-9. Returns
3
3
  // the exit status; writes one JSON document through `write`.
4
- import { readFileSync, writeFileSync } from 'node:fs';
5
- import { resolve } from 'node:path';
4
+ import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
5
+ import { join, resolve } from 'node:path';
6
6
  import { anchorGit, envelopeFor, seal, sealFiles, timeStampRequest } from './seal.mjs';
7
7
  import { parseAllowedSigners } from './sshsig.mjs';
8
- import { EvidenceRefusal, evidenceDirFrom } from './store.mjs';
8
+ import { EvidenceRefusal, createExclusive, evidenceDirFrom, syncClose, writeAll } from './store.mjs';
9
9
  import { verifyEvidence } from './verify.mjs';
10
+ import { EVIDENCE_RESULT_SCHEMA_VERSION } from '../version.mjs';
11
+ import { evidenceClausesFor } from '../compliance.mjs';
10
12
 
11
13
  const VERBS = ['verify', 'seal', 'anchor', 'sign'];
12
14
 
15
+ // The clauses a verified directory's evidence answers: a run journal's records, and the chain once
16
+ // it verifies.
17
+ function complianceOf(dir, verdict) {
18
+ const runs = join(dir, 'runs');
19
+ const journals = existsSync(runs) && readdirSync(runs).some((n) => n.endsWith('.jsonl'));
20
+ const present = journals ? ['journal-records', ...(verdict.verified ? ['journal-chain'] : [])] : [];
21
+ return present.flatMap((evidenceId) => evidenceClausesFor(evidenceId).map((c) => ({ evidenceId, ...c })));
22
+ }
23
+
13
24
  function anchorNewest(dir, opts) {
14
25
  const seals = sealFiles(dir);
15
26
  if (!seals.length) throw new EvidenceRefusal('there is no seal to anchor; run evidence seal first');
@@ -22,7 +33,12 @@ function anchorNewest(dir, opts) {
22
33
  if (opts.tsq) {
23
34
  const req = timeStampRequest(Buffer.from(text, 'utf8'));
24
35
  writeFileSync(resolve(opts.tsq), req.der);
25
- out.tsq = { path: resolve(opts.tsq), sha256: req.digest, nonce: req.nonce };
36
+ // The request is kept beside its seal, so verify can check the response's nonce against it.
37
+ const kept = join(dir, 'seals', `${newest.seq}.tsq`);
38
+ if (existsSync(kept)) unlinkSync(kept);
39
+ const fd = createExclusive(kept);
40
+ try { writeAll(fd, req.der); } finally { syncClose(fd); }
41
+ out.tsq = { path: resolve(opts.tsq), sha256: req.digest, nonce: req.nonce, kept: `seals/${newest.seq}.tsq` };
26
42
  }
27
43
  return out;
28
44
  }
@@ -41,14 +57,17 @@ export function evidenceCommand(verb, opts, { toolVersion, write, env = process.
41
57
  }
42
58
  const dir = evidenceDirFrom(opts, env);
43
59
  if (!dir) throw new EvidenceRefusal('evidence needs --evidence <dir> or COBOLWORK_EVIDENCE');
44
- const emit = (doc) => write(`${JSON.stringify({ tool: 'cobolwork-evidence', verb, ...doc }, null, opts.quiet ? 0 : 1)}\n`);
60
+ const emit = (doc) => write(`${JSON.stringify({ tool: 'cobolwork-evidence', schemaVersion: EVIDENCE_RESULT_SCHEMA_VERSION, verb, ...doc }, null, opts.quiet ? 0 : 1)}\n`);
45
61
 
46
62
  if (verb === 'verify') {
47
63
  const allowed = opts.allowedSigners ? parseAllowedSigners(readFileSync(resolve(opts.allowedSigners), 'utf8')) : null;
48
64
  const max = opts.maxUnsealed === undefined ? 0 : Number(opts.maxUnsealed);
49
65
  if (!Number.isSafeInteger(max) || max < 0) throw new EvidenceRefusal(`--max-unsealed takes a whole number; got ${opts.maxUnsealed}`);
50
- const verdict = verifyEvidence(dir, { allowed, anchorGit: opts.anchorGit ? resolve(opts.anchorGit) : null, anchorRef: opts.ref || null, maxUnsealed: max, env });
51
- emit(verdict);
66
+ if (opts.anchorPin && !opts.anchorGit) throw new EvidenceRefusal('--anchor-pin names a commit of the --anchor-git witness');
67
+ const file = (x) => (x ? resolve(x) : null);
68
+ const cosign = opts.cosignBundle ? { bundle: file(opts.cosignBundle), key: file(opts.cosignKey), identity: opts.certificateIdentity || null, issuer: opts.certificateOidcIssuer || null, trustedRoot: file(opts.trustedRoot), ignoreTlog: opts.insecureIgnoreTlog === true } : null;
69
+ const verdict = verifyEvidence(dir, { allowed, anchorGit: file(opts.anchorGit), anchorRef: opts.ref || null, anchorPin: opts.anchorPin || null, tsr: file(opts.tsr), tsaCa: file(opts.tsaCa), cosign, maxUnsealed: max, env });
70
+ emit({ ...verdict, compliance: complianceOf(dir, verdict) });
52
71
  return verdict.exit;
53
72
  }
54
73
  if (verb === 'seal') {
@@ -21,7 +21,8 @@ const strOrNull = (x) => x === null || str(x);
21
21
  const any = () => true;
22
22
  const strings = (x) => Array.isArray(x) && x.every(str);
23
23
 
24
- // kind -> { field: check }; a field listed as required must be present.
24
+ // kind -> { field: check }; a field listed as required must be present, and `check`, where a kind
25
+ // has one, names what its fields fail together.
25
26
  export const KINDS = {
26
27
  open: { fields: { tool: str, toolVersion: str, toolRevision: any, command: str, argv: strings, roots: strings, node: str, platform: str }, required: ['tool', 'toolVersion', 'command', 'argv', 'roots'] },
27
28
  input: { fields: { root: int, path: str, sha256: hex64, bytes: int }, required: ['root', 'path', 'sha256'] },
@@ -31,19 +32,26 @@ export const KINDS = {
31
32
  witness: { fields: { fingerprint: str, outcome: str, who: strOrNull, when: strOrNull, system: strOrNull, sourceSha256: hex64 }, required: ['fingerprint', 'outcome'] },
32
33
  verdict: { fields: { verdict: str, checks: any, relaxed: strings, exit: int }, required: ['verdict'] },
33
34
  output: { fields: { name: str, sha256: hex64, bytes: int, path: str, stdout: (x) => x === true }, required: ['name', 'sha256'] },
34
- close: { fields: { exit: (x) => x === null || int(x), counts: any, durationMs: int, ledger: str }, required: ['exit'] },
35
+ // ironwork's run journal closes with the program's true RETURN-CODE, which may be negative.
36
+ close: { fields: { exit: (x) => x === null || Number.isSafeInteger(x), counts: any, durationMs: int, ledger: str, executor: (x) => ['vm', 'interpreter'].includes(x) }, required: ['exit'] },
35
37
  // Written by ironwork's run journal, in this format, so one verifier reads both tools.
36
38
  dd: { fields: { dd: str, event: (x) => ['open', 'close', 'end'].includes(x), mode: str, sha256: hex64, bytes: int }, required: ['dd', 'event'] },
37
39
  call: { fields: { program: str, from: str, sha256: hex64 }, required: ['program'] },
38
40
  abend: { fields: { code: str, file: str, line: int }, required: ['code'] },
39
41
  step: { fields: { step: str, pgm: str, outcome: str }, required: ['step', 'pgm', 'outcome'] },
40
- sink: { fields: { sink: str, file: str, line: int, marker: str, reached: (x) => typeof x === 'boolean' }, required: ['sink', 'file', 'line', 'marker', 'reached'] },
41
- genesis: { fields: { createdAt: str, rotatedFrom: any }, required: ['createdAt'] },
42
+ sink: {
43
+ fields: { sink: str, file: str, line: int, marker: str, reached: (x) => typeof x === 'boolean', input: (x) => x === null || typeof x === 'boolean' },
44
+ required: ['sink', 'file', 'line'],
45
+ check: (f) => ((f.marker === undefined) !== (f.reached === undefined) ? 'marker and reached come together'
46
+ : f.marker === undefined && f.input === undefined ? 'needs marker and reached, or input' : null),
47
+ },
48
+ statement: { fields: { file: str, line: int, capped: (x) => x === true }, required: ['file', 'line'] },
49
+ genesis: { fields: { createdAt: str }, required: ['createdAt'] },
42
50
  run: { fields: { run: str, runChain: hex32, runLength: int, runTip: hex64 }, required: ['run', 'runChain', 'runLength', 'runTip'] },
43
- 'lock-broken': { fields: { holderPid: int, ageMs: int }, required: ['holderPid', 'ageMs'] },
51
+ 'lock-broken': { fields: { holderPid: (x) => x === null || int(x), ageMs: int }, required: ['holderPid', 'ageMs'] },
44
52
  };
45
53
 
46
- export const JOURNAL_KINDS = new Set(['open', 'input', 'finding', 'suppressed', 'baseline-write', 'witness', 'verdict', 'output', 'close', 'dd', 'call', 'abend', 'step', 'sink']);
54
+ export const JOURNAL_KINDS = new Set(['open', 'input', 'finding', 'suppressed', 'baseline-write', 'witness', 'verdict', 'output', 'close', 'dd', 'call', 'abend', 'step', 'sink', 'statement']);
47
55
  export const LEDGER_KINDS = new Set(['genesis', 'run', 'lock-broken']);
48
56
 
49
57
  export const newChain = () => randomBytes(16).toString('hex');
@@ -80,6 +88,8 @@ export function checkFields(kind, fields) {
80
88
  if (v !== undefined && !check(v)) throw new TypeError(`${kind}: ${k} does not hold what the spec says it holds`);
81
89
  }
82
90
  for (const k of spec.required) if (fields[k] === undefined) throw new TypeError(`${kind}: ${k} is required`);
91
+ const fault = spec.check?.(fields);
92
+ if (fault) throw new TypeError(`${kind}: ${fault}`);
83
93
  }
84
94
 
85
95
  // The next record after `prev` ({ chain, seq, hash } or null for the first), as its line.
@@ -159,6 +159,23 @@ export function resolveRef(repo, ref, env = process.env) {
159
159
  return r.status === 0 ? String(r.stdout).trim() : null;
160
160
  }
161
161
 
162
+ // Whether commit `older` is in the history of `newer`.
163
+ export function isAncestor(repo, older, newer, env = process.env) {
164
+ return git(repo, ['merge-base', '--is-ancestor', older, newer], env).status === 0;
165
+ }
166
+
167
+ // The blob of every seal the witness holds at `ref`, by its path: Map(`<chain>/<seq>.dsse.json` -> blob id).
168
+ export function witnessedBlobs(repo, ref, env = process.env) {
169
+ const r = git(repo, ['ls-tree', '-r', ref], env);
170
+ const out = new Map();
171
+ if (r.status !== 0) return out;
172
+ for (const line of String(r.stdout).split('\n')) {
173
+ const m = /^\d+ blob ([0-9a-f]+)\t([0-9a-f]{32}\/\d+\.dsse\.json)$/.exec(line.trim());
174
+ if (m) out.set(m[2], m[1]);
175
+ }
176
+ return out;
177
+ }
178
+
162
179
  // A DER TimeStampReq (RFC 3161) over SHA-256 of `bytes`, with a random nonce and certReq true.
163
180
  export function timeStampRequest(bytes) {
164
181
  const der = (tag, body) => {
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // The evidence directory on disk: where it may be, how its files are opened (never through a
3
3
  // symbolic link, owner-only), the ledger lock, and reading a chain file back line by line.
4
- import { closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, statSync, unlinkSync, writeSync } from 'node:fs';
4
+ import { closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, renameSync, statSync, unlinkSync, writeSync } from 'node:fs';
5
5
  import { basename, dirname, join, resolve, sep } from 'node:path';
6
6
 
7
7
  const { O_WRONLY, O_CREAT, O_EXCL, O_APPEND, O_NOFOLLOW } = constants;
@@ -138,32 +138,61 @@ export function lastLine(path) {
138
138
  const pidAlive = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } };
139
139
  const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
140
140
 
141
+ // A lock file as read: its inode and text, the pid it names and its age, or null when there is none.
142
+ // A lock whose pid and time cannot be read was left by a writer that died between creating it and
143
+ // writing them; it is aged from its modification time.
144
+ function readLock(path, now) {
145
+ let fd;
146
+ try { fd = openRead(path); } catch { return null; }
147
+ try {
148
+ const st = fstatSync(fd);
149
+ const text = readAll(fd).toString('utf8');
150
+ const [p, t] = text.trim().split(/\s+/).map(Number);
151
+ const pid = Number.isSafeInteger(p) && p > 0 ? p : null;
152
+ const since = Number.isSafeInteger(t) && t > 0 ? t : null;
153
+ return { ino: st.ino, text, pid, age: pid !== null && since !== null ? now() - since : now() - st.mtimeMs };
154
+ } finally { closeSync(fd); }
155
+ }
156
+
157
+ const isStale = (lock, staleMs) => lock !== null && lock.age > staleMs && (lock.pid === null || !pidAlive(lock.pid));
158
+
159
+ // Replaces the stale lock `seen` with this writer's own. Breakers hold ledger.lock.break while they
160
+ // break, so one breaks at a time, and rename it over the lock only while the lock is still the file
161
+ // judged stale: a lock a peer took since is never moved. Returns whether this writer holds the lock.
162
+ export function breakStale(path, seen, { staleMs = 60000, now = Date.now } = {}) {
163
+ const claim = `${path}.break`;
164
+ let fd;
165
+ try { fd = createExclusive(claim); } catch (e) {
166
+ if (e.code !== 'EEXIST') throw e;
167
+ if (isStale(readLock(claim, now), staleMs)) { try { unlinkSync(claim); } catch { /* already gone */ } }
168
+ return false;
169
+ }
170
+ writeAll(fd, `${process.pid} ${now()}\n`);
171
+ closeSync(fd);
172
+ const still = readLock(path, now);
173
+ if (still && still.ino === seen.ino && still.text === seen.text) {
174
+ try { renameSync(claim, path); return true; } catch { /* the claim is removed below */ }
175
+ }
176
+ try { unlinkSync(claim); } catch { /* already gone */ }
177
+ return false;
178
+ }
179
+
141
180
  // Takes ledger.lock, breaking one that is older than staleMs and whose holder is gone. Returns
142
181
  // { release, broken } or null when the lock could not be had within waitMs.
143
182
  export function takeLock(path, { waitMs = 5000, staleMs = 60000, now = Date.now } = {}) {
144
183
  const deadline = now() + waitMs;
145
- let broken = null;
184
+ const held = (broken) => ({ release: () => { try { unlinkSync(path); } catch { /* already gone */ } }, broken });
146
185
  for (;;) {
147
186
  try {
148
187
  const fd = createExclusive(path);
149
188
  writeAll(fd, `${process.pid} ${now()}\n`);
150
189
  closeSync(fd);
151
- return { release: () => { try { unlinkSync(path); } catch { /* already gone */ } }, broken };
190
+ return held(null);
152
191
  } catch (e) {
153
192
  if (e.code !== 'EEXIST') throw e;
154
193
  }
155
- let pid = null;
156
- let since = null;
157
- try {
158
- const [p, t] = readFileSync(path, 'utf8').trim().split(/\s+/).map(Number);
159
- pid = Number.isSafeInteger(p) ? p : null;
160
- since = Number.isSafeInteger(t) ? t : null;
161
- } catch { /* removed between the open and the read */ }
162
- const age = since === null ? null : now() - since;
163
- if (pid !== null && age !== null && age > staleMs && !pidAlive(pid)) {
164
- try { unlinkSync(path); broken = { holderPid: pid, ageMs: Math.max(0, Math.trunc(age)) }; } catch { /* a peer broke it first */ }
165
- continue;
166
- }
194
+ const seen = readLock(path, now);
195
+ if (isStale(seen, staleMs) && breakStale(path, seen, { staleMs, now })) return held({ holderPid: seen.pid, ageMs: Math.max(0, Math.trunc(seen.age)) });
167
196
  if (now() >= deadline) return null;
168
197
  sleep(25);
169
198
  }
@@ -0,0 +1,83 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // RFC 3161 as the git witness's sibling (docs/spec/evidence.md §8): the request `anchor --tsq`
3
+ // keeps beside a seal, and the facts of a time-stamp response cobolwork checks itself, its status,
4
+ // message imprint, nonce and time. The TSA's signature is OpenSSL's to check.
5
+
6
+ const SHA256_OID = Buffer.from([0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]);
7
+
8
+ // One DER element at `at`: its tag and where its contents start and end.
9
+ function element(buf, at, end = buf.length) {
10
+ if (at + 2 > end) throw new Error('a DER element runs past the end');
11
+ const tag = buf[at];
12
+ let length = buf[at + 1];
13
+ let start = at + 2;
14
+ if (length & 0x80) {
15
+ const n = length & 0x7f;
16
+ if (n < 1 || n > 4 || start + n > end) throw new Error('a DER length is not one this reads');
17
+ length = 0;
18
+ for (let i = 0; i < n; i++) length = length * 256 + buf[start + i];
19
+ start += n;
20
+ }
21
+ if (start + length > end) throw new Error('a DER element runs past the end');
22
+ return { tag, start, end: start + length };
23
+ }
24
+
25
+ function children(buf, parent) {
26
+ const out = [];
27
+ for (let at = parent.start; at < parent.end;) {
28
+ const c = element(buf, at, parent.end);
29
+ out.push(c);
30
+ at = c.end;
31
+ }
32
+ return out;
33
+ }
34
+
35
+ const contents = (buf, el) => buf.subarray(el.start, el.end);
36
+ const expect = (el, tag, what) => {
37
+ if (!el || el.tag !== tag) throw new Error(`${what} is not where RFC 3161 puts it`);
38
+ return el;
39
+ };
40
+ // An INTEGER's magnitude as hex, leading zero bytes dropped, so 00 80 and 80 compare equal.
41
+ const integerHex = (bytes) => {
42
+ let i = 0;
43
+ while (i < bytes.length - 1 && bytes[i] === 0) i++;
44
+ return Buffer.from(bytes.subarray(i)).toString('hex');
45
+ };
46
+
47
+ function imprintOf(buf, el, what) {
48
+ const [algorithm, hashed] = children(buf, expect(el, 0x30, `${what}'s message imprint`));
49
+ const oid = expect(children(buf, expect(algorithm, 0x30, `${what}'s hash algorithm`))[0], 0x06, `${what}'s hash algorithm`);
50
+ if (!contents(buf, oid).equals(SHA256_OID)) throw new Error(`${what} is not over SHA-256`);
51
+ return Buffer.from(contents(buf, expect(hashed, 0x04, `${what}'s hashed message`))).toString('hex');
52
+ }
53
+
54
+ // The message imprint and nonce of a TimeStampReq that `anchor --tsq` wrote.
55
+ export function readRequest(der) {
56
+ const [version, imprint, ...rest] = children(der, expect(element(der, 0), 0x30, 'the request'));
57
+ expect(version, 0x02, "the request's version");
58
+ const nonce = rest.find((e) => e.tag === 0x02);
59
+ return { digest: imprintOf(der, imprint, 'the request'), nonce: nonce ? integerHex(contents(der, nonce)) : null };
60
+ }
61
+
62
+ // status, digest (hex), nonce (hex or null) and genTime of a TimeStampResp; throws on a response
63
+ // whose shape is not RFC 3161's.
64
+ export function readResponse(der) {
65
+ const [statusInfo, token] = children(der, expect(element(der, 0), 0x30, 'the response'));
66
+ const status = parseInt(integerHex(contents(der, expect(children(der, expect(statusInfo, 0x30, 'the status'))[0], 0x02, 'the status'))), 16);
67
+ if (status > 1 || !token) return { status, digest: null, nonce: null, genTime: null };
68
+ const [, signedWrapper] = children(der, expect(token, 0x30, 'the time-stamp token'));
69
+ const signedData = children(der, expect(signedWrapper, 0xa0, 'the signed data'))[0];
70
+ const encapsulated = children(der, expect(signedData, 0x30, 'the signed data'))[2];
71
+ const [, contentWrapper] = children(der, expect(encapsulated, 0x30, 'the encapsulated content'));
72
+ const octets = children(der, expect(contentWrapper, 0xa0, 'the TSTInfo'))[0];
73
+ const tst = element(der, expect(octets, 0x04, 'the TSTInfo').start, octets.end);
74
+ const fields = children(der, expect(tst, 0x30, 'the TSTInfo'));
75
+ const genTime = expect(fields[4], 0x18, 'genTime');
76
+ const nonce = fields.slice(5).find((e) => e.tag === 0x02);
77
+ return {
78
+ status,
79
+ digest: imprintOf(der, fields[2], 'the response'),
80
+ nonce: nonce ? integerHex(contents(der, nonce)) : null,
81
+ genTime: contents(der, genTime).toString('latin1'),
82
+ };
83
+ }