kryptheon-night 0.1.4 → 0.1.6

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
@@ -76,19 +76,25 @@ attacks, this time, lost*.
76
76
 
77
77
  ## Prove the fix worked
78
78
 
79
- Paste the fix into Lovable or Cursor, let it deploy, then:
79
+ Paste the fix into Lovable or Cursor, let it deploy, then run the same command
80
+ again:
80
81
 
81
82
  ```
82
- npx kryptheon-night --recheck
83
+ npx kryptheon-night
83
84
  ```
84
85
 
85
- It runs the same attacks again and compares. A problem that *disappeared* is
86
- not the same as a problem that was *fixed* — if a table could not be tested
87
- this time, or is no longer there, it says so rather than crediting you with a
88
- fix. Close everything, with nothing left untested, and it hands out a badge.
86
+ It compares by itself — there is no flag to remember. A problem that
87
+ *disappeared* is not the same as a problem that was *fixed*: if a table could
88
+ not be tested this time, or is no longer there, it says so rather than
89
+ crediting you with a fix. Close everything, with nothing left untested, and it
90
+ hands out a badge.
89
91
 
90
92
  ## Or let it run every night, on its own
91
93
 
94
+ You do not have to go looking for this — the command above offers it once the
95
+ report is on the screen, and tells you if it is already running. If you would
96
+ rather just say so:
97
+
92
98
  ```
93
99
  npx kryptheon-night install
94
100
  ```
@@ -41,15 +41,20 @@ function usage() {
41
41
  '',
42
42
  ' kryptheon-night - attacks a copy of your database and tells you what got in.',
43
43
  '',
44
- ' Run it with nothing and it will ask you for what it needs:',
44
+ ' There is one command. It asks for what it needs:',
45
45
  '',
46
46
  ' npx kryptheon-night',
47
47
  '',
48
- ' Or let it do the same thing every night, from inside your database:',
48
+ ' Run it again after you fix something and it tells you what is actually',
49
+ ' closed - you do not have to ask it to. It also offers to do the same',
50
+ ' check every night from inside your database, and says so if it already',
51
+ ' is. Nothing below is needed to use this.',
52
+ '',
53
+ ' For scripts, and for people who like knowing:',
49
54
  '',
50
- ' npx kryptheon-night install set up the nightly run',
51
55
  ' npx kryptheon-night night read back the last night it ran',
52
56
  ' npx kryptheon-night status is it installed, and is it running',
57
+ ' npx kryptheon-night install set the nightly run up without being asked',
53
58
  ' npx kryptheon-night uninstall take it all out again',
54
59
  '',
55
60
  ' The nightly run needs pg_cron and pg_net. Supabase has both. It cannot',
@@ -58,11 +63,11 @@ function usage() {
58
63
  '',
59
64
  ' Options, none of them necessary:',
60
65
  '',
61
- ' --recheck run the same attacks again after a fix, and say',
62
- ' which problems are actually closed',
63
66
  ' --schema NAME the part of the database your app lives in.',
64
67
  ' Leave it out; it is "public" for almost everyone',
65
68
  ' --yes skip the "may I?" question. For scripts only',
69
+ ' --recheck nothing any more - comparing against the last run',
70
+ ' happens by itself. Kept so scripts do not break',
66
71
  ' --help this',
67
72
  '',
68
73
  ' The connection string can be put in KN_DATABASE_URL instead of being',
@@ -218,6 +223,105 @@ async function runVerb(command, client, target) {
218
223
  throw new Error('there is no command called ' + command);
219
224
  }
220
225
 
226
+ /**
227
+ * After the report: say what the nightly run is doing, or offer it.
228
+ *
229
+ * Three states and each says something different. Installed and running gets
230
+ * one line, because it is working and nobody needs a paragraph about it.
231
+ * Installed with no job is the quiet failure - the schema is there, the
232
+ * functions are there, and nothing runs - so it says so plainly. Not
233
+ * installed gets the offer.
234
+ *
235
+ * Nothing here can make the scan fail. Whatever happens, the person has
236
+ * already been given their answer, and losing it to an error about a nightly
237
+ * job they never asked for would be the tool wasting the only thing it did.
238
+ */
239
+ async function offerTheNight(client, target, canAsk) {
240
+ let where = null;
241
+ try {
242
+ where = await installer.status(client, {});
243
+ } catch (err) {
244
+ return;
245
+ }
246
+
247
+ if (where && where.job) {
248
+ line(' I am also checking this every night, at ' + where.job.schedule + '.');
249
+ line(' Read the last one with: npx kryptheon-night night');
250
+ line('');
251
+ return;
252
+ }
253
+
254
+ if (where && !where.job) {
255
+ line(' The nightly check is installed here but nothing is scheduled, so it');
256
+ line(' is not running. Setting it up again would fix that:');
257
+ line('');
258
+ line(' npx kryptheon-night install');
259
+ line('');
260
+ return;
261
+ }
262
+
263
+ if (!canAsk) {
264
+ // Nobody to ask. Said once, quietly, rather than nagging a log file.
265
+ line(' This can also run every night by itself: npx kryptheon-night install');
266
+ line('');
267
+ return;
268
+ }
269
+
270
+ const can = await installer.extensionState(client, 'pg_net').catch(() => ({ available: false }));
271
+ const cron = await installer.extensionState(client, 'pg_cron').catch(() => ({ available: false }));
272
+ if (!can.available || !cron.available) {
273
+ // Not offered where it cannot happen. An offer that fails when accepted
274
+ // is worse than no offer.
275
+ return;
276
+ }
277
+
278
+ line(' One more thing.');
279
+ line('');
280
+ line(' What you just read is true about your app right now. It stops being');
281
+ line(' true the next time anybody changes it - and that is the day nobody');
282
+ line(' runs this. I can do exactly the same check every night, from inside');
283
+ line(' your database, and have the answer waiting.');
284
+ line('');
285
+ const yes = await intro.askYesNo(' Set that up? (y/n) ');
286
+ if (!yes) {
287
+ line('');
288
+ line(' Fine. It is here if you change your mind:');
289
+ line(' npx kryptheon-night install');
290
+ line('');
291
+ return;
292
+ }
293
+
294
+ block(intro.installConsentLines(installer.SCHEMA, target, installer.AT));
295
+ const sure = await intro.askYesNo(' Go ahead? (y/n) ');
296
+ if (!sure) {
297
+ line('');
298
+ line(' Left alone. Nothing was installed.');
299
+ line('');
300
+ return;
301
+ }
302
+
303
+ try {
304
+ const done = await installer.install(client, { source: target });
305
+ line('');
306
+ line(' Done. It will run at ' + done.at + ', watching "' + done.source + '".');
307
+ if (done.made.extensions.length) {
308
+ line(' I added ' + done.made.extensions.join(' and ') + '; uninstall takes them back out.');
309
+ }
310
+ line('');
311
+ line(' Tomorrow: npx kryptheon-night night');
312
+ line(' To remove: npx kryptheon-night uninstall');
313
+ line('');
314
+ } catch (err) {
315
+ // The scan still stands. Say what failed and leave it at that.
316
+ line('');
317
+ line(' I could not set up the nightly run: ' + String(err.message).split('\n')[0]);
318
+ line('');
319
+ line(' The report above is still good. Nothing was left half-installed.');
320
+ line('');
321
+ await installer.uninstall(client, {}).catch(() => {});
322
+ }
323
+ }
324
+
221
325
  /** Prints a block of plain lines with the indent the rest of the report uses. */
222
326
  function block(lines, write) {
223
327
  (write || line)('');
@@ -311,17 +415,31 @@ async function main() {
311
415
  }
312
416
  }
313
417
 
418
+ // Compared against last time whenever there IS a last time, without being
419
+ // asked.
420
+ //
421
+ // `--recheck` was a flag, and a flag is a thing somebody has to know about.
422
+ // The person this is for fixed what the report told them to fix and typed
423
+ // the same command again - and got a fresh report that said the same number
424
+ // of problems, with no word about which of them they had just closed. The
425
+ // tool knew. It had the file. It waited to be asked.
426
+ //
427
+ // The flag still works, because anything scripted may be passing it, and
428
+ // now it changes nothing: with an earlier run here, this always compares.
314
429
  const file = path.resolve(scanner.LAST_RUN);
315
- const before = asked.recheck ? scanner.loadLastRun(file) : null;
430
+ const before = scanner.loadLastRun(file);
316
431
  if (asked.recheck && !before) {
432
+ // Asked for explicitly and impossible, which is worth saying. Arriving
433
+ // here by accident is not: without the flag, no earlier run simply means
434
+ // this is the first one.
317
435
  fail('');
318
436
  fail(' There is no earlier run in this folder to compare against.');
319
437
  fail('');
320
- fail(' --recheck proves a fix worked by running the same attacks again and');
438
+ fail(' A re-check proves a fix worked by running the same attacks again and');
321
439
  fail(' comparing. With nothing to compare against it would report every');
322
440
  fail(' problem as new, which would look like an answer and would not be one.');
323
441
  fail('');
324
- fail(' Run it without --recheck first.');
442
+ fail(' Run it once first, and it will compare by itself from then on.');
325
443
  fail('');
326
444
  process.exit(2);
327
445
  }
@@ -372,15 +490,25 @@ async function main() {
372
490
  scanner.report(result);
373
491
  scanner.saveRun(file, result);
374
492
  process.exitCode = result.stopped ? 2 : result.findings.length ? 1 : 0;
375
- return;
493
+ } else {
494
+ const verdict = recheck.compare(before, result);
495
+ recheck.describe(verdict).forEach(line);
496
+ recheck.badgeLines(verdict, result.attacksRun || 0).forEach(line);
497
+ if (result.findings.length) scanner.report(result);
498
+ scanner.saveRun(file, result);
499
+ process.exitCode = result.stopped ? 2 : verdict.allClear ? 0 : 1;
376
500
  }
377
501
 
378
- const verdict = recheck.compare(before, result);
379
- recheck.describe(verdict).forEach(line);
380
- recheck.badgeLines(verdict, result.attacksRun || 0).forEach(line);
381
- if (result.findings.length) scanner.report(result);
382
- scanner.saveRun(file, result);
383
- process.exitCode = result.stopped ? 2 : verdict.allClear ? 0 : 1;
502
+ // And then the only other thing worth doing, offered rather than
503
+ // documented.
504
+ //
505
+ // `install` was a verb, and a verb is a thing somebody has to find out
506
+ // about. Nobody who clicked Deploy in Lovable is going to read a list of
507
+ // subcommands - they will run this once, get their answer, and never
508
+ // think about it again, which is exactly the app that is unprotected
509
+ // three months later. The tool already knows whether the nightly run is
510
+ // there. So it asks.
511
+ if (!result.stopped) await offerTheNight(client, target, canAsk);
384
512
  } catch (err) {
385
513
  // Anything that went wrong mid-scan. The copy has already been dropped by
386
514
  // the `finally` inside the scan itself, so there is nothing to clean up