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 +12 -6
- package/bin/kryptheon-night.js +143 -15
- package/engine.sql +2540 -2484
- package/installer.js +15 -0
- package/package.json +3 -2
- package/schema.js +1137 -1058
- package/trouble.js +19 -0
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
|
|
83
|
+
npx kryptheon-night
|
|
83
84
|
```
|
|
84
85
|
|
|
85
|
-
It
|
|
86
|
-
not the same as a problem that was *fixed
|
|
87
|
-
this time, or is no longer there, it says so rather than
|
|
88
|
-
fix. Close everything, with nothing left untested, and it
|
|
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
|
```
|
package/bin/kryptheon-night.js
CHANGED
|
@@ -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
|
-
'
|
|
44
|
+
' There is one command. It asks for what it needs:',
|
|
45
45
|
'',
|
|
46
46
|
' npx kryptheon-night',
|
|
47
47
|
'',
|
|
48
|
-
'
|
|
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 =
|
|
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('
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|