kryptheon-night 0.1.0
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 +158 -0
- package/attack.js +555 -0
- package/bin/kryptheon-night.js +248 -0
- package/collision.js +309 -0
- package/connect.js +60 -0
- package/finding.js +552 -0
- package/intro.js +202 -0
- package/orphan.js +230 -0
- package/package.json +73 -0
- package/recheck.js +219 -0
- package/scan.js +569 -0
- package/schema.js +922 -0
- package/tamper.js +208 -0
- package/trouble.js +377 -0
package/intro.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// What the person is asked, and what they are told before anything runs.
|
|
2
|
+
//
|
|
3
|
+
// This file is the whole difference between a tool for people who write code
|
|
4
|
+
// and a tool for people who do not. The engine behind it does not change; the
|
|
5
|
+
// first ninety seconds do.
|
|
6
|
+
//
|
|
7
|
+
// Two things happen here and both are deliberate.
|
|
8
|
+
//
|
|
9
|
+
// Asking, rather than requiring. `KN_DATABASE_URL=... node scan.js public` is
|
|
10
|
+
// three unfamiliar ideas at once: an environment variable, a connection
|
|
11
|
+
// string, and a schema. Somebody who has only ever clicked Deploy has met
|
|
12
|
+
// none of them. So the command takes no arguments, and asks.
|
|
13
|
+
//
|
|
14
|
+
// Telling before doing. This tool connects to a person's production database
|
|
15
|
+
// with the owner's credential and creates things in it. Nobody should agree
|
|
16
|
+
// to that from a package name. What it does, what it does not do, and then a
|
|
17
|
+
// yes or no - in that order, before the first statement is sent.
|
|
18
|
+
|
|
19
|
+
const readline = require('readline');
|
|
20
|
+
const trouble = require('./trouble.js');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Where the connection string is, said as buttons rather than as concepts.
|
|
24
|
+
*
|
|
25
|
+
* Named screen by screen because "get your connection string" is not an
|
|
26
|
+
* instruction to somebody who does not know the phrase. Supabase first
|
|
27
|
+
* because that is who this is for; Lovable and Bolt build on Supabase, so
|
|
28
|
+
* their users end up on the same page by a different door.
|
|
29
|
+
*/
|
|
30
|
+
function whereToFindIt() {
|
|
31
|
+
return [
|
|
32
|
+
'Where to find it:',
|
|
33
|
+
'',
|
|
34
|
+
' Supabase supabase.com/dashboard -> your project -> the gear icon',
|
|
35
|
+
' (Project Settings) at the bottom left -> Database ->',
|
|
36
|
+
' scroll to "Connection string" -> the URI tab -> Copy.',
|
|
37
|
+
'',
|
|
38
|
+
' Replace [YOUR-PASSWORD] in it with your database password.',
|
|
39
|
+
' It is on that same page under "Database password".',
|
|
40
|
+
'',
|
|
41
|
+
' Lovable your app uses Supabase underneath. Open the Supabase',
|
|
42
|
+
' and Bolt project it made for you and follow the lines above.',
|
|
43
|
+
'',
|
|
44
|
+
' Neon console.neon.tech -> your project -> Connection Details.',
|
|
45
|
+
'',
|
|
46
|
+
'It is one long line beginning postgresql:// and it ends in /postgres.',
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* What is about to happen, and what is not.
|
|
52
|
+
*
|
|
53
|
+
* The second half matters more than the first. Everybody who runs this is
|
|
54
|
+
* being asked to hand a stranger's program the key to their live database,
|
|
55
|
+
* and the fears they have are specific: will you read my customers, will you
|
|
56
|
+
* send anything anywhere, will you leave a mess. Each of those is answered in
|
|
57
|
+
* its own line, in the words they would use.
|
|
58
|
+
*
|
|
59
|
+
* Nothing here is a promise made only on this screen. Every line is something
|
|
60
|
+
* the code is built to make true and a check in this repo fails if it stops
|
|
61
|
+
* being true - `untouched.check.js` photographs the whole database and
|
|
62
|
+
* compares it afterwards.
|
|
63
|
+
*/
|
|
64
|
+
function consentLines(target) {
|
|
65
|
+
return [
|
|
66
|
+
'Before I touch anything, here is exactly what I am going to do.',
|
|
67
|
+
'',
|
|
68
|
+
'What I will do:',
|
|
69
|
+
'',
|
|
70
|
+
' - Look at the shape of your "' + target + '" tables: their names, their',
|
|
71
|
+
' columns, and the rules about who is allowed to see what.',
|
|
72
|
+
'',
|
|
73
|
+
' - Make one new temporary space inside your database and rebuild that',
|
|
74
|
+
' same shape in it. Your real tables are not changed.',
|
|
75
|
+
'',
|
|
76
|
+
' - Put two made-up people in the copy - fake names, fake emails.',
|
|
77
|
+
'',
|
|
78
|
+
' - Attack the copy. I try to read those two fake people as a stranger',
|
|
79
|
+
' would, try to change their rows, try to break them.',
|
|
80
|
+
'',
|
|
81
|
+
' - Tell you what got through, and delete the copy.',
|
|
82
|
+
'',
|
|
83
|
+
'What I will not do:',
|
|
84
|
+
'',
|
|
85
|
+
' - I do not read your real data. Not one customer, order or message.',
|
|
86
|
+
' Every row I read is a row I put there myself a moment earlier.',
|
|
87
|
+
'',
|
|
88
|
+
' - I do not change your real tables. Only the temporary copy.',
|
|
89
|
+
'',
|
|
90
|
+
' - I do not send anything anywhere. No account, no upload, no server of',
|
|
91
|
+
' mine. Your connection string stays on this computer and is not',
|
|
92
|
+
' written to any file.',
|
|
93
|
+
'',
|
|
94
|
+
' - I do not leave anything behind. The copy is deleted when I finish,',
|
|
95
|
+
' and also if I crash.',
|
|
96
|
+
'',
|
|
97
|
+
'This takes two or three minutes.',
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Asks a question and hands back what was typed. */
|
|
102
|
+
function ask(question) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
105
|
+
rl.question(question, (answer) => {
|
|
106
|
+
rl.close();
|
|
107
|
+
resolve(String(answer || '').trim());
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Asks for something that must not be echoed.
|
|
114
|
+
*
|
|
115
|
+
* The connection string carries the password to the entire database. Shown on
|
|
116
|
+
* screen it is read by whoever is behind them, and it stays in the scrollback
|
|
117
|
+
* of whatever terminal they are in for as long as that window is open.
|
|
118
|
+
*
|
|
119
|
+
* Nothing is printed as they type - not even stars, because a pasted string
|
|
120
|
+
* of stars is no more checkable than nothing and the character count alone
|
|
121
|
+
* says how long the password is. What they get instead is the address read
|
|
122
|
+
* back to them afterwards, with the password removed, which is the part they
|
|
123
|
+
* would actually want to check.
|
|
124
|
+
*/
|
|
125
|
+
function askSecret(question) {
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
128
|
+
let silent = false;
|
|
129
|
+
// readline writes every keystroke back to the screen itself. This replaces
|
|
130
|
+
// that with nothing, but only once the prompt has been written - otherwise
|
|
131
|
+
// the question never appears either.
|
|
132
|
+
rl._writeToOutput = function (text) {
|
|
133
|
+
if (!silent) rl.output.write(text);
|
|
134
|
+
};
|
|
135
|
+
rl.question(question, (answer) => {
|
|
136
|
+
silent = false;
|
|
137
|
+
rl.output.write('\n');
|
|
138
|
+
rl.close();
|
|
139
|
+
resolve(String(answer || '').trim());
|
|
140
|
+
});
|
|
141
|
+
silent = true;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Was that a yes?
|
|
147
|
+
*
|
|
148
|
+
* The question being answered is "may I connect to your production database",
|
|
149
|
+
* so the rule is that only a yes is a yes. A stray newline, a shrug, an
|
|
150
|
+
* accidental Enter on an empty line - none of those are permission, and the
|
|
151
|
+
* cost of getting this backwards is connecting to somebody's live database
|
|
152
|
+
* without being asked to.
|
|
153
|
+
*
|
|
154
|
+
* Separate from the asking so it can be checked directly. Every form of this
|
|
155
|
+
* that hid inside a prompt went unchecked.
|
|
156
|
+
*/
|
|
157
|
+
function isYes(answer) {
|
|
158
|
+
const said = String(answer == null ? '' : answer).trim().toLowerCase();
|
|
159
|
+
return said === 'y' || said === 'yes';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Yes or no, where anything that is not a clear yes is a no. */
|
|
163
|
+
async function askYesNo(question) {
|
|
164
|
+
return isYes(await ask(question));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The connection string, from wherever it can be had.
|
|
169
|
+
*
|
|
170
|
+
* The environment variable is checked first so that anybody scripting this
|
|
171
|
+
* never sees a prompt, and so the string does not have to be retyped on every
|
|
172
|
+
* run. Nothing is saved: asked again next time is the correct behaviour for a
|
|
173
|
+
* credential this powerful, and a file holding it would be the one thing this
|
|
174
|
+
* tool tells people not to do.
|
|
175
|
+
*/
|
|
176
|
+
async function askForConnection(say) {
|
|
177
|
+
say('');
|
|
178
|
+
say(' I need the connection string for your database.');
|
|
179
|
+
say('');
|
|
180
|
+
say(' It is one line that lets me connect. It contains your database');
|
|
181
|
+
say(' password, so I will not show it on screen as you paste it, and I do');
|
|
182
|
+
say(' not save it anywhere.');
|
|
183
|
+
say('');
|
|
184
|
+
whereToFindIt().forEach((l) => say(' ' + l));
|
|
185
|
+
say('');
|
|
186
|
+
|
|
187
|
+
const given = await askSecret(' Paste it here and press Enter: ');
|
|
188
|
+
return given;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
whereToFindIt: whereToFindIt,
|
|
193
|
+
consentLines: consentLines,
|
|
194
|
+
ask: ask,
|
|
195
|
+
askSecret: askSecret,
|
|
196
|
+
askYesNo: askYesNo,
|
|
197
|
+
isYes: isYes,
|
|
198
|
+
askForConnection: askForConnection,
|
|
199
|
+
// Re-exported so the command has one place to reach for the wording of a
|
|
200
|
+
// failure, rather than two.
|
|
201
|
+
withoutSecret: trouble.withoutSecret,
|
|
202
|
+
};
|
package/orphan.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// The interruption attack: can a half-finished write survive?
|
|
2
|
+
//
|
|
3
|
+
// A request cut off partway is mostly a question about the app's code - were
|
|
4
|
+
// the two inserts wrapped in a transaction? - and this tool never sees the
|
|
5
|
+
// app's code. Guessing would be the lost update all over again.
|
|
6
|
+
//
|
|
7
|
+
// But there is a half of it the database answers on its own. A foreign key is
|
|
8
|
+
// what makes a half-finished state impossible to keep, no matter how badly the
|
|
9
|
+
// app behaves or where the connection drops. Without one, an order can name a
|
|
10
|
+
// customer who does not exist, and nothing will ever notice.
|
|
11
|
+
//
|
|
12
|
+
// So the attack is: put in a row pointing at something that is not there, and
|
|
13
|
+
// see whether the database takes it.
|
|
14
|
+
//
|
|
15
|
+
// WHAT THAT ACTUALLY COSTS SOMEBODY (measured in probe-orphan.js)
|
|
16
|
+
//
|
|
17
|
+
// - with no foreign key the row is accepted, and nothing in the database
|
|
18
|
+
// will ever find it again
|
|
19
|
+
// - with one, the insert is refused
|
|
20
|
+
// - with one, deleting the parent is refused too - so "delete my account"
|
|
21
|
+
// really does take the data with it, or fails loudly. Without one, the
|
|
22
|
+
// customer is gone and their rows are still sitting there.
|
|
23
|
+
//
|
|
24
|
+
// WHAT IS DELIBERATELY NOT REPORTED
|
|
25
|
+
//
|
|
26
|
+
// A column with no table it could point at. `stripe_id`, `session_id`,
|
|
27
|
+
// `external_id` reference something outside this database entirely, and
|
|
28
|
+
// telling somebody to add a foreign key to one would be telling them to break
|
|
29
|
+
// their app. Only a column whose name matches a real table here, whose type
|
|
30
|
+
// matches that table's key, is ever considered.
|
|
31
|
+
//
|
|
32
|
+
// Log and audit tables are left alone too. Keeping the id of something that
|
|
33
|
+
// has since been deleted is the entire point of an audit row, and a foreign
|
|
34
|
+
// key there would be the bug.
|
|
35
|
+
|
|
36
|
+
const { quote } = require('./schema.js');
|
|
37
|
+
const attack = require('./attack.js');
|
|
38
|
+
|
|
39
|
+
// Tables whose job is to remember things after they are gone. A dangling id in
|
|
40
|
+
// an audit row is the feature.
|
|
41
|
+
const KEEPS_HISTORY = /(^|_)(log|logs|audit|audits|event|events|history|archive|archives|snapshot|snapshots|activity|activities)(_|$)/i;
|
|
42
|
+
|
|
43
|
+
/** The single-column primary key of a table, or null if it has none. */
|
|
44
|
+
function primaryKeyOf(table) {
|
|
45
|
+
for (const constraint of table.constraints || []) {
|
|
46
|
+
if (constraint.kind !== 'p') continue;
|
|
47
|
+
const match = /PRIMARY KEY \(([^)]+)\)/i.exec(constraint.definition);
|
|
48
|
+
if (!match) continue;
|
|
49
|
+
const columns = match[1].split(',').map((name) => name.trim().split('"').join(''));
|
|
50
|
+
// A composite key is not something a single `<thing>_id` column points at.
|
|
51
|
+
return columns.length === 1 ? columns[0] : null;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The table a column named `<thing>_id` is pointing at, if there is one.
|
|
58
|
+
*
|
|
59
|
+
* The name has to match a table that is really here, and the types have to
|
|
60
|
+
* agree. Both, because `stripe_id` matches nothing and `org_id integer` does
|
|
61
|
+
* not point at an `orgs.id` that is a uuid.
|
|
62
|
+
*/
|
|
63
|
+
function parentFor(column, tables) {
|
|
64
|
+
const stem = /^(.+)_id$/i.exec(column.name);
|
|
65
|
+
if (!stem) return null;
|
|
66
|
+
const wanted = stem[1].toLowerCase();
|
|
67
|
+
const parent = (tables || []).find((table) => {
|
|
68
|
+
const name = table.name.toLowerCase();
|
|
69
|
+
return name === wanted || name === wanted + 's' || name === wanted + 'es';
|
|
70
|
+
});
|
|
71
|
+
if (!parent) return null;
|
|
72
|
+
|
|
73
|
+
const key = primaryKeyOf(parent);
|
|
74
|
+
if (!key) return null;
|
|
75
|
+
const keyColumn = (parent.columns || []).find((c) => c.name === key);
|
|
76
|
+
if (!keyColumn || keyColumn.type !== column.type) return null;
|
|
77
|
+
|
|
78
|
+
return { table: parent, keyColumn: key, type: keyColumn.type };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Is this column already held down by a foreign key? */
|
|
82
|
+
function alreadyTied(table, columnName) {
|
|
83
|
+
return attack.foreignKeys(table).some((key) => key.columns.includes(columnName));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every column that looks like it points somewhere, with whether it is already
|
|
88
|
+
* tied down.
|
|
89
|
+
*
|
|
90
|
+
* Tied columns are attacked too, the same way the collision attack races
|
|
91
|
+
* columns that already have a unique index. Skipping them would mean that the
|
|
92
|
+
* moment somebody adds the foreign key this asked for, the attack stops
|
|
93
|
+
* running - and the re-check could no longer watch it be refused, so it would
|
|
94
|
+
* report the fix it requested as "could not confirm".
|
|
95
|
+
*
|
|
96
|
+
* `tied` decides what is reported, never what is attempted.
|
|
97
|
+
*/
|
|
98
|
+
function candidates(tables) {
|
|
99
|
+
const found = [];
|
|
100
|
+
for (const table of tables || []) {
|
|
101
|
+
if (KEEPS_HISTORY.test(table.name)) continue;
|
|
102
|
+
for (const column of table.columns || []) {
|
|
103
|
+
const parent = parentFor(column, tables);
|
|
104
|
+
if (!parent) continue;
|
|
105
|
+
found.push({
|
|
106
|
+
table: table.name,
|
|
107
|
+
column: column.name,
|
|
108
|
+
parent: parent.table.name,
|
|
109
|
+
parentKey: parent.keyColumn,
|
|
110
|
+
type: parent.type,
|
|
111
|
+
tied: alreadyTied(table, column.name),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return found;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A value of the right type that is certainly not in the parent table. */
|
|
119
|
+
function nobody(type) {
|
|
120
|
+
const kind = String(type).toLowerCase();
|
|
121
|
+
if (kind === 'uuid') return '99999999-9999-4999-8999-999999999999';
|
|
122
|
+
if (/^(integer|bigint|smallint)/.test(kind)) return 2147480000;
|
|
123
|
+
if (/^(numeric|decimal|real|double)/.test(kind)) return 2147480000;
|
|
124
|
+
return 'kryptheon-nobody';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Point a row at something that is not there, and see if it is taken.
|
|
129
|
+
*
|
|
130
|
+
* Run as the owner of the schema on purpose. The question is not who is
|
|
131
|
+
* allowed to create an orphan - it is whether the database permits one to
|
|
132
|
+
* exist at all, which is what decides whether a dropped connection can leave
|
|
133
|
+
* one behind.
|
|
134
|
+
*
|
|
135
|
+
* Rolled back, always. Nothing this does survives the statement that did it.
|
|
136
|
+
*/
|
|
137
|
+
async function orphan(client, schema, tables, seeded) {
|
|
138
|
+
const findings = [];
|
|
139
|
+
const completed = [];
|
|
140
|
+
const notTried = [];
|
|
141
|
+
const shapeFor = new Map((seeded || []).map((entry) => [entry.table, entry.attempt || 0]));
|
|
142
|
+
const byName = new Map((tables || []).map((table) => [table.name, table]));
|
|
143
|
+
|
|
144
|
+
for (const target of candidates(tables)) {
|
|
145
|
+
const key = 'orphaned:' + target.table + ':' + target.column;
|
|
146
|
+
const table = byName.get(target.table);
|
|
147
|
+
const missing = nobody(target.type);
|
|
148
|
+
|
|
149
|
+
// It only proves anything if the value really is absent from the parent.
|
|
150
|
+
try {
|
|
151
|
+
const { rows } = await client.query(
|
|
152
|
+
'SELECT 1 FROM ' + quote(schema) + '.' + quote(target.parent) +
|
|
153
|
+
' WHERE ' + quote(target.parentKey) + ' = $1 LIMIT 1',
|
|
154
|
+
[missing],
|
|
155
|
+
);
|
|
156
|
+
if (rows.length) {
|
|
157
|
+
notTried.push({ table: target.table, column: target.column, why: 'the test value was already in ' + target.parent });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
notTried.push({ table: target.table, column: target.column, why: 'could not look in ' + target.parent + ': ' + err.message });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const override = {};
|
|
166
|
+
override[target.column] = missing;
|
|
167
|
+
let row;
|
|
168
|
+
try {
|
|
169
|
+
row = await attack.rowFor(client, schema, table, attack.USER_C, 9, override, shapeFor.get(target.table));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
notTried.push({ table: target.table, column: target.column, why: err.message });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await client.query('BEGIN');
|
|
176
|
+
let landed = false;
|
|
177
|
+
let refused = null;
|
|
178
|
+
try {
|
|
179
|
+
await client.query("SET LOCAL statement_timeout = '15s'");
|
|
180
|
+
await attack.insertRow(client, schema, target.table, row);
|
|
181
|
+
landed = true;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
refused = err.message;
|
|
184
|
+
} finally {
|
|
185
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!landed && !/violates foreign key constraint/i.test(String(refused))) {
|
|
189
|
+
// Turned away by something other than referential integrity, so nothing
|
|
190
|
+
// was learned about whether an orphan can exist.
|
|
191
|
+
notTried.push({ table: target.table, column: target.column, why: refused });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
completed.push(key);
|
|
196
|
+
// Whether a key is supposedly there does not come into it. What is reported
|
|
197
|
+
// is what landed.
|
|
198
|
+
//
|
|
199
|
+
// There was a `&& !target.tied` here, on the reasoning that a tied column
|
|
200
|
+
// could not produce an orphan anyway. It could not - measured on Neon, a
|
|
201
|
+
// foreign key cannot be quietly switched off: DISABLE TRIGGER ALL is
|
|
202
|
+
// refused because the constraint triggers are system triggers, and DISABLE
|
|
203
|
+
// TRIGGER USER does not touch them. So the guard never fired, and a guard
|
|
204
|
+
// that never fires is one more thing that can be wrong. If a row naming
|
|
205
|
+
// nobody ever does land on a column with a key on it, that is worse news
|
|
206
|
+
// than usual and saying so is right.
|
|
207
|
+
if (landed) {
|
|
208
|
+
findings.push({
|
|
209
|
+
kind: 'orphaned',
|
|
210
|
+
table: target.table,
|
|
211
|
+
column: target.column,
|
|
212
|
+
parent: target.parent,
|
|
213
|
+
parentKey: target.parentKey,
|
|
214
|
+
columns: (table.columns || []).map((c) => c.name),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { findings: findings, completed: completed, notTried: notTried };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
KEEPS_HISTORY: KEEPS_HISTORY,
|
|
224
|
+
primaryKeyOf: primaryKeyOf,
|
|
225
|
+
parentFor: parentFor,
|
|
226
|
+
alreadyTied: alreadyTied,
|
|
227
|
+
candidates: candidates,
|
|
228
|
+
nobody: nobody,
|
|
229
|
+
orphan: orphan,
|
|
230
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kryptheon-night",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Attacks a copy of your Supabase database and tells you in plain English what got in.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"kryptheon-night": "bin/kryptheon-night.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"scan.js",
|
|
13
|
+
"schema.js",
|
|
14
|
+
"attack.js",
|
|
15
|
+
"finding.js",
|
|
16
|
+
"collision.js",
|
|
17
|
+
"tamper.js",
|
|
18
|
+
"orphan.js",
|
|
19
|
+
"recheck.js",
|
|
20
|
+
"intro.js",
|
|
21
|
+
"trouble.js",
|
|
22
|
+
"connect.js",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"check": "node package.check.js && node trouble.check.js && node intro.check.js && node finding.check.js && node recheck.check.js && node untouched.check.js && node blocked.check.js && node orphans.check.js && node guests.check.js && node usable.check.js && node twin.check.js && node external.check.js && node schema.check.js && node collision.check.js && node tamper.check.js && node orphan.check.js && node shapes.check.js && node verdicts.check.js && node loop.check.js",
|
|
30
|
+
"check:dry": "node package.check.js && node trouble.check.js && node intro.check.js && node finding.check.js && node recheck.check.js",
|
|
31
|
+
"check:package": "node package.check.js",
|
|
32
|
+
"check:trouble": "node trouble.check.js",
|
|
33
|
+
"check:intro": "node intro.check.js",
|
|
34
|
+
"check:report": "node finding.check.js",
|
|
35
|
+
"check:recheck": "node recheck.check.js",
|
|
36
|
+
"check:copy": "node schema.check.js",
|
|
37
|
+
"check:collision": "node collision.check.js",
|
|
38
|
+
"check:loop": "node loop.check.js",
|
|
39
|
+
"probe": "node probe-rls.js",
|
|
40
|
+
"probe:collision": "node probe-collision.js",
|
|
41
|
+
"demo": "node demo.js",
|
|
42
|
+
"check:promise": "node untouched.check.js",
|
|
43
|
+
"check:blocked": "node blocked.check.js",
|
|
44
|
+
"check:external": "node external.check.js",
|
|
45
|
+
"check:shapes": "node shapes.check.js",
|
|
46
|
+
"check:verdicts": "node verdicts.check.js",
|
|
47
|
+
"check:writes": "node tamper.check.js",
|
|
48
|
+
"check:orphans": "node orphans.check.js",
|
|
49
|
+
"probe:write": "node probe-write.js",
|
|
50
|
+
"check:usable": "node usable.check.js",
|
|
51
|
+
"check:guests": "node guests.check.js",
|
|
52
|
+
"check:interruption": "node orphan.check.js",
|
|
53
|
+
"probe:orphan": "node probe-orphan.js",
|
|
54
|
+
"check:twin": "node twin.check.js",
|
|
55
|
+
"prepublishOnly": "node package.check.js"
|
|
56
|
+
},
|
|
57
|
+
"keywords": [
|
|
58
|
+
"supabase",
|
|
59
|
+
"postgres",
|
|
60
|
+
"rls",
|
|
61
|
+
"row-level-security",
|
|
62
|
+
"security",
|
|
63
|
+
"lovable",
|
|
64
|
+
"cli"
|
|
65
|
+
],
|
|
66
|
+
"repository": {
|
|
67
|
+
"type": "git",
|
|
68
|
+
"url": "git+https://github.com/jhashantanu987-beep/kryptheon-night.git"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"pg": "^8.23.0"
|
|
72
|
+
}
|
|
73
|
+
}
|