kryptheon-night 0.1.3 → 0.1.5
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 +45 -6
- package/bin/kryptheon-night.js +310 -18
- package/engine.sql +2484 -0
- package/installer.js +224 -0
- package/intro.js +52 -0
- package/package.json +9 -4
- package/scan.js +38 -7
- package/sqlengine.js +200 -0
package/README.md
CHANGED
|
@@ -76,16 +76,55 @@ 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
|
|
84
|
+
```
|
|
85
|
+
|
|
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.
|
|
91
|
+
|
|
92
|
+
## Or let it run every night, on its own
|
|
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
|
+
|
|
98
|
+
```
|
|
99
|
+
npx kryptheon-night install
|
|
83
100
|
```
|
|
84
101
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
This one **stays**, and it tells you so before it does anything. It puts the
|
|
103
|
+
checking functions into a schema called `kryptheon`, adds `pg_cron` and
|
|
104
|
+
`pg_net` if they are not already there, and schedules one job at 3am. Then
|
|
105
|
+
every night it does what the command above does — copies the shape of your
|
|
106
|
+
tables, attacks the copy, writes down what got through, deletes the copy.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
npx kryptheon-night night read back the last night it ran
|
|
110
|
+
npx kryptheon-night status is it installed, and is it running
|
|
111
|
+
npx kryptheon-night uninstall take it all out again
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Nothing leaves your database**, because the checking happens inside it — the
|
|
115
|
+
connection string is not stored anywhere and nothing is uploaded. It is also
|
|
116
|
+
much faster for the same reason: on a real project the command line took 2
|
|
117
|
+
minutes 16 seconds and the nightly run took 1 second, and both found the same
|
|
118
|
+
five problems.
|
|
119
|
+
|
|
120
|
+
**One thing it cannot do.** Racing two requests against each other — the "can
|
|
121
|
+
this exist twice" question — needs two connections at the same instant, and
|
|
122
|
+
nothing living inside a database has them. The nightly report names every
|
|
123
|
+
column it could not race. Running the command by hand still tests them
|
|
124
|
+
properly.
|
|
125
|
+
|
|
126
|
+
`uninstall` removes the job, the schema, and any extension it had to add, and
|
|
127
|
+
leaves alone anything that was already there.
|
|
89
128
|
|
|
90
129
|
## Three answers, three exit codes
|
|
91
130
|
|
package/bin/kryptheon-night.js
CHANGED
|
@@ -30,6 +30,8 @@ const recheck = require('../recheck.js');
|
|
|
30
30
|
const intro = require('../intro.js');
|
|
31
31
|
const trouble = require('../trouble.js');
|
|
32
32
|
const { howToConnect } = require('../connect.js');
|
|
33
|
+
const installer = require('../installer.js');
|
|
34
|
+
const finding = require('../finding.js');
|
|
33
35
|
|
|
34
36
|
const line = (text) => process.stdout.write(text + '\n');
|
|
35
37
|
const fail = (text) => process.stderr.write(text + '\n');
|
|
@@ -39,17 +41,33 @@ function usage() {
|
|
|
39
41
|
'',
|
|
40
42
|
' kryptheon-night - attacks a copy of your database and tells you what got in.',
|
|
41
43
|
'',
|
|
42
|
-
'
|
|
44
|
+
' There is one command. It asks for what it needs:',
|
|
43
45
|
'',
|
|
44
46
|
' npx kryptheon-night',
|
|
45
47
|
'',
|
|
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:',
|
|
54
|
+
'',
|
|
55
|
+
' npx kryptheon-night night read back the last night it ran',
|
|
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',
|
|
58
|
+
' npx kryptheon-night uninstall take it all out again',
|
|
59
|
+
'',
|
|
60
|
+
' The nightly run needs pg_cron and pg_net. Supabase has both. It cannot',
|
|
61
|
+
' race two requests at once, so it is weaker than this command for the',
|
|
62
|
+
' "can this exist twice" question, and its report says which columns.',
|
|
63
|
+
'',
|
|
46
64
|
' Options, none of them necessary:',
|
|
47
65
|
'',
|
|
48
|
-
' --recheck run the same attacks again after a fix, and say',
|
|
49
|
-
' which problems are actually closed',
|
|
50
66
|
' --schema NAME the part of the database your app lives in.',
|
|
51
67
|
' Leave it out; it is "public" for almost everyone',
|
|
52
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',
|
|
53
71
|
' --help this',
|
|
54
72
|
'',
|
|
55
73
|
' The connection string can be put in KN_DATABASE_URL instead of being',
|
|
@@ -58,9 +76,17 @@ function usage() {
|
|
|
58
76
|
];
|
|
59
77
|
}
|
|
60
78
|
|
|
79
|
+
// The things this can be asked to do besides scan. Kept to verbs nobody
|
|
80
|
+
// names a schema: `public` is the schema almost everybody has, and a bare
|
|
81
|
+
// word that is not one of these is still read as a schema so that
|
|
82
|
+
// `kryptheon-night public` keeps working.
|
|
83
|
+
const VERBS = ['install', 'uninstall', 'status', 'night'];
|
|
84
|
+
|
|
61
85
|
/** What was asked for, and what was left to the default. */
|
|
62
86
|
function readArgs(argv) {
|
|
63
|
-
const asked = {
|
|
87
|
+
const asked = {
|
|
88
|
+
command: 'scan', schema: null, recheck: false, yes: false, help: false, unknown: [],
|
|
89
|
+
};
|
|
64
90
|
for (let i = 0; i < argv.length; i++) {
|
|
65
91
|
const arg = argv[i];
|
|
66
92
|
if (arg === '--recheck') asked.recheck = true;
|
|
@@ -69,14 +95,233 @@ function readArgs(argv) {
|
|
|
69
95
|
else if (arg === '--schema') asked.schema = argv[++i] || null;
|
|
70
96
|
else if (arg.startsWith('--schema=')) asked.schema = arg.slice('--schema='.length);
|
|
71
97
|
else if (arg.startsWith('-')) asked.unknown.push(arg);
|
|
72
|
-
//
|
|
73
|
-
//
|
|
98
|
+
// The first bare word is a verb if it is one, and otherwise the schema -
|
|
99
|
+
// so that somebody who has read the old instructions and types
|
|
100
|
+
// `kryptheon-night public` is not told off. Anybody whose schema really
|
|
101
|
+
// is called `status` can say so with --schema.
|
|
102
|
+
else if (asked.command === 'scan' && !asked.schema && VERBS.includes(arg)) asked.command = arg;
|
|
74
103
|
else if (!asked.schema) asked.schema = arg;
|
|
75
104
|
else asked.unknown.push(arg);
|
|
76
105
|
}
|
|
77
106
|
return asked;
|
|
78
107
|
}
|
|
79
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The three doors that are not a scan.
|
|
111
|
+
*
|
|
112
|
+
* Each returns the exit code, because anything automating this reads those
|
|
113
|
+
* and not the words: 0 for nothing got through or nothing to say, 1 for
|
|
114
|
+
* something did, 2 for could not.
|
|
115
|
+
*/
|
|
116
|
+
async function runVerb(command, client, target) {
|
|
117
|
+
if (command === 'status') {
|
|
118
|
+
const where = await installer.status(client, {});
|
|
119
|
+
if (!where) {
|
|
120
|
+
line(' The nightly run is not installed in this database.');
|
|
121
|
+
line('');
|
|
122
|
+
line(' npx kryptheon-night install would set it up.');
|
|
123
|
+
line('');
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
line(' Installed, and watching "' + where.source + '".');
|
|
127
|
+
line('');
|
|
128
|
+
if (where.job) {
|
|
129
|
+
line(' job ' + where.job.jobname + ' ' + where.job.schedule +
|
|
130
|
+
(where.job.active ? '' : ' (NOT ACTIVE)'));
|
|
131
|
+
} else {
|
|
132
|
+
// The one failure that looks like success from every other angle: the
|
|
133
|
+
// schema is there, the functions are there, and nothing is running.
|
|
134
|
+
line(' job MISSING - nothing is scheduled, so nothing runs');
|
|
135
|
+
}
|
|
136
|
+
line(' schema ' + where.schema);
|
|
137
|
+
line(' since ' + new Date(where.installed_at).toISOString().slice(0, 16).replace('T', ' '));
|
|
138
|
+
line('');
|
|
139
|
+
const { rows } = await client.query(
|
|
140
|
+
'SELECT count(*)::int AS n, max(ran_at) AS last FROM ' +
|
|
141
|
+
'"' + String(where.schema).split('"').join('""') + '".runs',
|
|
142
|
+
).catch(() => ({ rows: [{ n: 0, last: null }] }));
|
|
143
|
+
line(' runs ' + rows[0].n +
|
|
144
|
+
(rows[0].last ? ' last ' + new Date(rows[0].last).toISOString().slice(0, 16).replace('T', ' ') : ''));
|
|
145
|
+
line('');
|
|
146
|
+
return where.job ? 0 : 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (command === 'install') {
|
|
150
|
+
line(' Installing ...');
|
|
151
|
+
const done = await installer.install(client, { source: target });
|
|
152
|
+
line('');
|
|
153
|
+
line(' Installed. It will run at ' + done.at + ', watching "' + done.source + '".');
|
|
154
|
+
line('');
|
|
155
|
+
if (done.made.extensions.length) {
|
|
156
|
+
line(' I had to add: ' + done.made.extensions.join(', ') + '.');
|
|
157
|
+
line(' Removing Kryptheon will take those back out again.');
|
|
158
|
+
line('');
|
|
159
|
+
}
|
|
160
|
+
line(' Tomorrow morning: npx kryptheon-night night');
|
|
161
|
+
line(' To remove it: npx kryptheon-night uninstall');
|
|
162
|
+
line('');
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (command === 'uninstall') {
|
|
167
|
+
const was = await installer.status(client, {});
|
|
168
|
+
if (!was) {
|
|
169
|
+
line(' There is nothing installed here to remove.');
|
|
170
|
+
line('');
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
const removed = await installer.uninstall(client, {});
|
|
174
|
+
line(' Removed.');
|
|
175
|
+
line('');
|
|
176
|
+
line(' the nightly job ' + (removed.job ? 'gone' : 'there was none'));
|
|
177
|
+
line(' the schema ' + (removed.schema ? 'gone' : 'left, it was not mine to drop'));
|
|
178
|
+
line(' extensions ' + (removed.extensions.length
|
|
179
|
+
? removed.extensions.join(', ') + ' - the ones I added'
|
|
180
|
+
: 'none removed; they were here before me'));
|
|
181
|
+
line('');
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (command === 'night') {
|
|
186
|
+
const where = await installer.status(client, {});
|
|
187
|
+
if (!where) {
|
|
188
|
+
line(' The nightly run is not installed in this database, so there is');
|
|
189
|
+
line(' nothing to read back.');
|
|
190
|
+
line('');
|
|
191
|
+
line(' npx kryptheon-night would scan it now.');
|
|
192
|
+
line(' npx kryptheon-night install would set up the nightly run.');
|
|
193
|
+
line('');
|
|
194
|
+
return 2;
|
|
195
|
+
}
|
|
196
|
+
const quoted = '"' + String(where.schema).split('"').join('""') + '"';
|
|
197
|
+
const { rows } = await client.query(
|
|
198
|
+
'SELECT ran_at, source, stopped, attacks_run, findings, not_checked ' +
|
|
199
|
+
'FROM ' + quoted + '.runs ORDER BY ran_at DESC LIMIT 1',
|
|
200
|
+
);
|
|
201
|
+
if (!rows.length) {
|
|
202
|
+
// Installed and never run is not the same as run and found nothing, and
|
|
203
|
+
// saying "nothing got through" here would be the worst sentence in the
|
|
204
|
+
// product printed about a night that never happened.
|
|
205
|
+
line(' It is installed, and it has not run yet.');
|
|
206
|
+
line('');
|
|
207
|
+
line(' The first run is at ' + (where.job ? where.job.schedule : 'whenever it is scheduled') + '.');
|
|
208
|
+
line('');
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
const run = rows[0];
|
|
212
|
+
line(' From the night of ' + new Date(run.ran_at).toISOString().slice(0, 16).replace('T', ' ') +
|
|
213
|
+
', on "' + run.source + '":');
|
|
214
|
+
scanner.report({
|
|
215
|
+
stopped: run.stopped,
|
|
216
|
+
attacksRun: run.attacks_run,
|
|
217
|
+
notChecked: run.not_checked || [],
|
|
218
|
+
findings: finding.describeAll(run.findings || []),
|
|
219
|
+
});
|
|
220
|
+
return run.stopped ? 2 : (run.findings || []).length ? 1 : 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw new Error('there is no command called ' + command);
|
|
224
|
+
}
|
|
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
|
+
|
|
80
325
|
/** Prints a block of plain lines with the indent the rest of the report uses. */
|
|
81
326
|
function block(lines, write) {
|
|
82
327
|
(write || line)('');
|
|
@@ -141,7 +386,11 @@ async function main() {
|
|
|
141
386
|
line('');
|
|
142
387
|
line(' Database: ' + trouble.withoutSecret(connection));
|
|
143
388
|
|
|
144
|
-
|
|
389
|
+
// Reading does not need permission; changing does. `status` and `night` only
|
|
390
|
+
// read what is already written down, so asking would be ceremony.
|
|
391
|
+
const needsAYes = asked.command === 'scan' || asked.command === 'install';
|
|
392
|
+
|
|
393
|
+
if (needsAYes && !asked.yes) {
|
|
145
394
|
if (!canAsk) {
|
|
146
395
|
fail('');
|
|
147
396
|
fail(' I will not connect to your database without being told yes, and');
|
|
@@ -151,7 +400,12 @@ async function main() {
|
|
|
151
400
|
fail('');
|
|
152
401
|
process.exit(2);
|
|
153
402
|
}
|
|
154
|
-
|
|
403
|
+
// Two different screens, because they promise different things. The scan's
|
|
404
|
+
// ends "I do not leave anything behind", which is true of a scan and false
|
|
405
|
+
// of an install - and this is the one screen the product cannot be loose on.
|
|
406
|
+
block(asked.command === 'install'
|
|
407
|
+
? intro.installConsentLines(installer.SCHEMA, target, installer.AT)
|
|
408
|
+
: intro.consentLines(target));
|
|
155
409
|
const yes = await intro.askYesNo(' Go ahead? (y/n) ');
|
|
156
410
|
if (!yes) {
|
|
157
411
|
line('');
|
|
@@ -161,17 +415,31 @@ async function main() {
|
|
|
161
415
|
}
|
|
162
416
|
}
|
|
163
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.
|
|
164
429
|
const file = path.resolve(scanner.LAST_RUN);
|
|
165
|
-
const before =
|
|
430
|
+
const before = scanner.loadLastRun(file);
|
|
166
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.
|
|
167
435
|
fail('');
|
|
168
436
|
fail(' There is no earlier run in this folder to compare against.');
|
|
169
437
|
fail('');
|
|
170
|
-
fail('
|
|
438
|
+
fail(' A re-check proves a fix worked by running the same attacks again and');
|
|
171
439
|
fail(' comparing. With nothing to compare against it would report every');
|
|
172
440
|
fail(' problem as new, which would look like an answer and would not be one.');
|
|
173
441
|
fail('');
|
|
174
|
-
fail(' Run it
|
|
442
|
+
fail(' Run it once first, and it will compare by itself from then on.');
|
|
175
443
|
fail('');
|
|
176
444
|
process.exit(2);
|
|
177
445
|
}
|
|
@@ -191,6 +459,20 @@ async function main() {
|
|
|
191
459
|
line(' Connected.');
|
|
192
460
|
line('');
|
|
193
461
|
|
|
462
|
+
// The other three doors. Each one ends the command; only `scan` falls
|
|
463
|
+
// through to the attack below.
|
|
464
|
+
if (asked.command !== 'scan') {
|
|
465
|
+
try {
|
|
466
|
+
process.exitCode = await runVerb(asked.command, client, target);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
block(trouble.explain(err, connection), fail);
|
|
469
|
+
process.exitCode = 2;
|
|
470
|
+
} finally {
|
|
471
|
+
await client.end().catch(() => {});
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
194
476
|
try {
|
|
195
477
|
const result = await scanner.scan(client, target, {
|
|
196
478
|
// Two requests arriving at the same instant cannot be faked down one
|
|
@@ -208,15 +490,25 @@ async function main() {
|
|
|
208
490
|
scanner.report(result);
|
|
209
491
|
scanner.saveRun(file, result);
|
|
210
492
|
process.exitCode = result.stopped ? 2 : result.findings.length ? 1 : 0;
|
|
211
|
-
|
|
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;
|
|
212
500
|
}
|
|
213
501
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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);
|
|
220
512
|
} catch (err) {
|
|
221
513
|
// Anything that went wrong mid-scan. The copy has already been dropped by
|
|
222
514
|
// the `finally` inside the scan itself, so there is nothing to clean up
|