kryptheon-night 0.1.5 → 0.1.7

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
@@ -37,6 +37,27 @@ If you paste the wrong thing — the project URL, an API key, the line with
37
37
  `[YOUR-PASSWORD]` still in it — it says so and tells you where the right one
38
38
  is. It does not just fail.
39
39
 
40
+ ## No connection string? Check your app from outside
41
+
42
+ ```
43
+ npx kryptheon-night https://your-app.example.com
44
+ ```
45
+
46
+ Give it your running app instead. It reads the app the way a stranger's
47
+ browser does — the Supabase address and public key every visitor is already
48
+ handed — and asks each table your app uses one question: can a stranger read
49
+ this? It asks for a **count only**, so no customer's row is ever pulled out,
50
+ and it writes nothing. About ten seconds, no password, no yes to give.
51
+
52
+ It is a narrower check than the full scan, and it says so:
53
+
54
+ - It only proves a leak when a stranger actually gets rows back. A table that
55
+ answers with none looks the same whether it is well protected or open and
56
+ empty, so it is reported as "could not tell", never as safe.
57
+ - One customer reading another's rows, adding or deleting rows, and the same
58
+ thing saved twice at once all change data, so they are only ever tried on
59
+ a copy — the full scan above.
60
+
40
61
  ## What you get back
41
62
 
42
63
  Four questions, asked against the copy:
@@ -32,6 +32,7 @@ const trouble = require('../trouble.js');
32
32
  const { howToConnect } = require('../connect.js');
33
33
  const installer = require('../installer.js');
34
34
  const finding = require('../finding.js');
35
+ const outside = require('../outside.js');
35
36
 
36
37
  const line = (text) => process.stdout.write(text + '\n');
37
38
  const fail = (text) => process.stderr.write(text + '\n');
@@ -45,6 +46,12 @@ function usage() {
45
46
  '',
46
47
  ' npx kryptheon-night',
47
48
  '',
49
+ ' No connection string? Give it your running app instead. It reads the app',
50
+ ' the way a stranger does and tells you which tables anyone can read -',
51
+ ' counts only, nothing written:',
52
+ '',
53
+ ' npx kryptheon-night https://your-app.example.com',
54
+ '',
48
55
  ' Run it again after you fix something and it tells you what is actually',
49
56
  ' closed - you do not have to ask it to. It also offers to do the same',
50
57
  ' check every night from inside your database, and says so if it already',
@@ -85,11 +92,15 @@ const VERBS = ['install', 'uninstall', 'status', 'night'];
85
92
  /** What was asked for, and what was left to the default. */
86
93
  function readArgs(argv) {
87
94
  const asked = {
88
- command: 'scan', schema: null, recheck: false, yes: false, help: false, unknown: [],
95
+ command: 'scan', schema: null, appUrl: null, recheck: false, yes: false, help: false, unknown: [],
89
96
  };
90
97
  for (let i = 0; i < argv.length; i++) {
91
98
  const arg = argv[i];
92
- if (arg === '--recheck') asked.recheck = true;
99
+ // An address is the outside attack: the running app, read the way a
100
+ // stranger's browser reads it. Never a schema name - no schema is called
101
+ // https://anything.
102
+ if (/^https?:\/\//i.test(arg) && !asked.appUrl) asked.appUrl = arg;
103
+ else if (arg === '--recheck') asked.recheck = true;
93
104
  else if (arg === '--yes' || arg === '-y') asked.yes = true;
94
105
  else if (arg === '--help' || arg === '-h') asked.help = true;
95
106
  else if (arg === '--schema') asked.schema = argv[++i] || null;
@@ -322,6 +333,45 @@ async function offerTheNight(client, target, canAsk) {
322
333
  }
323
334
  }
324
335
 
336
+ /**
337
+ * The attack from outside, end to end: find the backend in the app, ask each
338
+ * table as a stranger, say what came back and what could not be tested.
339
+ *
340
+ * Exit codes are the same three as the scan: 1 when a stranger read rows, 0
341
+ * when nothing came back, 2 when it could not run at all.
342
+ */
343
+ async function runOutside(appUrl) {
344
+ line('');
345
+ line(' Kryptheon Night Shift - from outside');
346
+ line(' I read your app the way a stranger\'s browser does, and ask your');
347
+ line(' database what it lets a stranger see. Counts only, nothing written.');
348
+ line('');
349
+ line(' Reading ' + appUrl + ' ...');
350
+
351
+ let found;
352
+ try {
353
+ found = await outside.discover(appUrl);
354
+ } catch (err) {
355
+ fail('');
356
+ fail(' ' + (err.code ? err.message : 'I could not read ' + appUrl + ': ' + err.message));
357
+ fail('');
358
+ return 2;
359
+ }
360
+
361
+ let result;
362
+ try {
363
+ result = await outside.prowl(found);
364
+ } catch (err) {
365
+ fail('');
366
+ fail(' I found your database but could not ask it anything: ' + err.message);
367
+ fail('');
368
+ return 2;
369
+ }
370
+
371
+ outside.reportLines(found, result).forEach(line);
372
+ return result.findings.length ? 1 : 0;
373
+ }
374
+
325
375
  /** Prints a block of plain lines with the indent the rest of the report uses. */
326
376
  function block(lines, write) {
327
377
  (write || line)('');
@@ -343,6 +393,15 @@ async function main() {
343
393
  process.exit(2);
344
394
  }
345
395
 
396
+ // The outside attack. Needs no connection string, no copy and no yes: it
397
+ // only asks the live app what it already answers any visitor, and asks for
398
+ // counts, never rows. So nothing below - the string, the consent screen,
399
+ // the connection - applies to it.
400
+ if (asked.appUrl) {
401
+ process.exitCode = await runOutside(asked.appUrl);
402
+ return;
403
+ }
404
+
346
405
  line('');
347
406
  line(' Kryptheon Night Shift');
348
407
  line(' I attack a copy of your database and tell you what got in.');