kryptheon-night 0.1.25 → 0.1.27

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 CHANGED
@@ -69,6 +69,17 @@ Four questions, asked against the copy:
69
69
  | **Collision** | can the same thing exist twice? |
70
70
  | **Interruption** | can a half-finished write survive? |
71
71
 
72
+ And three things it says are **yours to check**, because only you know
73
+ whether they are meant:
74
+
75
+ | check | the question |
76
+ |---|---|
77
+ | **Lowest role** | can a viewer in a team change or add to what is in it, and which roles can read its tokens, keys and payloads? (tried on the copy, undone) |
78
+ | **Storage** | is a bucket public although a rule says who may read it? (settings only, never a file) |
79
+ | **Open functions** | can a visitor with no account call a function that skips the rules? |
80
+
81
+ These are listed apart from problems and do not change the exit code.
82
+
72
83
  A problem reads like this:
73
84
 
74
85
  CRITICAL customers
package/attack.js CHANGED
@@ -204,6 +204,48 @@ function allowedByCheck(table, columnName) {
204
204
  return found;
205
205
  }
206
206
 
207
+ /**
208
+ * Sets of columns where exactly one may be filled in.
209
+ *
210
+ * Found on a blind test (HelixOps): documents belonged to a work order or to
211
+ * an inspection, never both, written
212
+ * CHECK ((work_order_id IS NOT NULL)::int + (inspection_id IS NOT NULL)::int = 1)
213
+ * Both parents were there, so the seeder pointed the row at both, the CHECK
214
+ * refused every attempt, and the table was never tested at all.
215
+ *
216
+ * Two spellings are read - that sum, and num_nonnulls(a, b, ...) = 1 - and
217
+ * only when the constraint says nothing else. A CHECK with anything more in it
218
+ * is left alone rather than half understood.
219
+ */
220
+ function exactlyOne(table) {
221
+ const groups = [];
222
+ const name = '("(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*)';
223
+ const unquote = (text) => (text.charAt(0) === '"' ? text.slice(1, -1).split('""').join('"') : text);
224
+ for (const constraint of table.constraints || []) {
225
+ if (constraint.kind !== 'c') continue;
226
+ const definition = String(constraint.definition || '');
227
+ let columns = [];
228
+ let rest = definition;
229
+ const summed = new RegExp('\\(\\(' + name + ' IS NOT NULL\\)\\)::integer', 'gi');
230
+ let match;
231
+ while ((match = summed.exec(definition)) !== null) columns.push(unquote(match[1]));
232
+ rest = rest.replace(summed, '').replace(/\+/g, '');
233
+ if (!columns.length) {
234
+ const counted = /num_nonnulls\(([^()]*)\)/i.exec(definition);
235
+ if (counted) {
236
+ const listed = new RegExp(name, 'g');
237
+ let one;
238
+ while ((one = listed.exec(counted[1])) !== null) columns.push(unquote(one[1]));
239
+ rest = rest.replace(counted[0], '');
240
+ }
241
+ }
242
+ // Nothing may be left but the CHECK itself, brackets and "= 1".
243
+ if (columns.length < 2 || rest.replace(/[()\s]/g, '').toUpperCase() !== 'CHECK=1') continue;
244
+ groups.push(columns);
245
+ }
246
+ return groups;
247
+ }
248
+
207
249
  /**
208
250
  * The foreign keys on a table, read out of Postgres's own wording.
209
251
  *
@@ -395,12 +437,28 @@ async function rowFor(client, schema, table, person, distinct, overrides, attemp
395
437
  const partOfKey = new Set();
396
438
  for (const key of foreignKeys(table)) key.columns.forEach((name) => partOfKey.add(name));
397
439
 
440
+ // Where exactly one of a set may be filled in: the one already chosen, or
441
+ // else the first that can be - a key needs a parent row to point at. The
442
+ // rest are written as nothing, whatever their default.
443
+ const chosen = (name) => Object.prototype.hasOwnProperty.call(forced, name);
444
+ const onlyOne = new Map();
445
+ for (const group of exactlyOne(table)) {
446
+ const kept = group.find(chosen) || group.find((name) => !partOfKey.has(name) || borrowed.has(name));
447
+ for (const name of group) onlyOne.set(name, name === kept);
448
+ }
449
+
398
450
  for (const column of table.columns) {
399
451
  if (Object.prototype.hasOwnProperty.call(forced, column.name)) {
400
452
  columns.push(column.name);
401
453
  values.push(forced[column.name]);
402
454
  continue;
403
455
  }
456
+ if (onlyOne.get(column.name) === false) {
457
+ columns.push(column.name);
458
+ values.push(null);
459
+ continue;
460
+ }
461
+ const mustFill = onlyOne.get(column.name) === true;
404
462
  if (column.name === owner) {
405
463
  columns.push(column.name);
406
464
  values.push(person);
@@ -436,9 +494,11 @@ async function rowFor(client, schema, table, person, distinct, overrides, attemp
436
494
  values.push(borrowed.get(column.name));
437
495
  continue;
438
496
  }
439
- // Anything with a default can supply its own value.
497
+ // Anything with a default can supply its own value. A column that may be
498
+ // empty is left empty - unless it is the one of a set that has to be
499
+ // filled in.
440
500
  if (column.default_expr) continue;
441
- if (!column.not_null) continue;
501
+ if (!column.not_null && !mustFill) continue;
442
502
  columns.push(column.name);
443
503
  // A CHECK that lists what it will accept beats anything invented here.
444
504
  const allowed = allowedByCheck(table, column.name);
@@ -766,6 +826,7 @@ module.exports = {
766
826
  dependencyOrder: dependencyOrder,
767
827
  valueFor: valueFor,
768
828
  allowedByCheck: allowedByCheck,
829
+ exactlyOne: exactlyOne,
769
830
  rowFor: rowFor,
770
831
  existingRow: existingRow,
771
832
  insertRow: insertRow,
@@ -648,7 +648,9 @@ async function main() {
648
648
  if (result.findings.length) scanner.report(result);
649
649
  else scanner.notTestedLines(result).forEach(line);
650
650
  scanner.saveRun(file, result);
651
- process.exitCode = result.stopped ? 2 : verdict.allClear ? 0 : 1;
651
+ // Two different databases: the verdict is a comparison, so the exit
652
+ // code is this run's own answer, the same as a first run.
653
+ process.exitCode = result.stopped ? 2 : verdict.elsewhere ? scanner.exitCodeFor(result) : verdict.allClear ? 0 : 1;
652
654
  }
653
655
 
654
656
  // And then the only other thing worth doing, offered rather than