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/recheck.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Did the fix actually work?
|
|
2
|
+
//
|
|
3
|
+
// This is the smallest piece of the product and the one people pay for. The
|
|
4
|
+
// loop only closes if somebody can paste a fix, run this, and be told the hole
|
|
5
|
+
// is shut - and that sentence is worth exactly as much as it is trustworthy.
|
|
6
|
+
// Anyone can print "Fixed". Printing it only when it is true is the whole job.
|
|
7
|
+
//
|
|
8
|
+
// One rule sits under everything here:
|
|
9
|
+
//
|
|
10
|
+
// A problem that disappeared is not the same as a problem that was fixed.
|
|
11
|
+
//
|
|
12
|
+
// A finding vanishes from the second run for several reasons, and only one of
|
|
13
|
+
// them is good news. The table might have been fixed. It might also have become
|
|
14
|
+
// impossible to test - a new constraint that seeding cannot satisfy, a renamed
|
|
15
|
+
// column, a table dropped outright. In every one of those the finding is gone
|
|
16
|
+
// from the list, and in none of them has anybody been made safe.
|
|
17
|
+
//
|
|
18
|
+
// So this compares the two runs against what the second run actually managed to
|
|
19
|
+
// try, not against what it happened to report.
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A finding's identity across runs: the attack, and what it was against.
|
|
23
|
+
*
|
|
24
|
+
* The column is part of it wherever there is one. A users table can accept a
|
|
25
|
+
* duplicate email AND a duplicate username; without the column those two share
|
|
26
|
+
* an identity, and adding a unique constraint to one of them would be read as
|
|
27
|
+
* having fixed both.
|
|
28
|
+
*/
|
|
29
|
+
function keyOf(item) {
|
|
30
|
+
// A third part when there is one: the column for a duplicate, or who the
|
|
31
|
+
// caller was for a write. Closing a table to strangers does not close it to
|
|
32
|
+
// signed-in customers, and sharing one identity would report the second as
|
|
33
|
+
// fixed the moment the first was.
|
|
34
|
+
const detail = item.column || item.who || '';
|
|
35
|
+
return item.kind + ':' + item.table + (detail ? ':' + detail : '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What the second run says about the first.
|
|
40
|
+
*
|
|
41
|
+
* `unverifiable` is the category that earns this module. Without it every
|
|
42
|
+
* untestable table would land in `fixed`, and a green badge would be handed to
|
|
43
|
+
* an app nobody checked.
|
|
44
|
+
*/
|
|
45
|
+
function compare(before, after) {
|
|
46
|
+
const was = (before && before.findings) || [];
|
|
47
|
+
const now = (after && after.findings) || [];
|
|
48
|
+
|
|
49
|
+
// The second run failed outright. Nothing can be claimed about anything.
|
|
50
|
+
if (after && after.stopped) {
|
|
51
|
+
return {
|
|
52
|
+
stopped: after.stopped,
|
|
53
|
+
fixed: [],
|
|
54
|
+
stillOpen: was,
|
|
55
|
+
unverifiable: was,
|
|
56
|
+
newlyBroken: [],
|
|
57
|
+
allClear: false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const nowByKey = new Map(now.map((item) => [keyOf(item), item]));
|
|
62
|
+
|
|
63
|
+
// What the second run actually attempted, named the same way a finding is.
|
|
64
|
+
// Per attack rather than per table, because one table can be attacked three
|
|
65
|
+
// different ways: a table that was seeded and read is not thereby a table
|
|
66
|
+
// whose unique columns were raced. Keyed by table alone, a collision attack
|
|
67
|
+
// that never ran would have looked like a collision that was refused.
|
|
68
|
+
const attempted = new Set((after && after.attempted) || []);
|
|
69
|
+
const skipped = (after && after.notChecked) || [];
|
|
70
|
+
const reasons = new Map();
|
|
71
|
+
for (const entry of skipped) {
|
|
72
|
+
if (entry.key) reasons.set(entry.key, entry.why);
|
|
73
|
+
if (entry.table && !reasons.has(entry.table)) reasons.set(entry.table, entry.why);
|
|
74
|
+
}
|
|
75
|
+
const tablesTouched = new Set(
|
|
76
|
+
Array.from(attempted).map((key) => String(key).split(':')[1]).filter(Boolean),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const fixed = [];
|
|
80
|
+
const stillOpen = [];
|
|
81
|
+
const unverifiable = [];
|
|
82
|
+
|
|
83
|
+
for (const item of was) {
|
|
84
|
+
const key = keyOf(item);
|
|
85
|
+
if (nowByKey.has(key)) {
|
|
86
|
+
stillOpen.push(item);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Gone from the list. Now the only question that matters: was it tried?
|
|
90
|
+
if (!attempted.has(key)) {
|
|
91
|
+
unverifiable.push(
|
|
92
|
+
Object.assign({}, item, {
|
|
93
|
+
why:
|
|
94
|
+
reasons.get(key) ||
|
|
95
|
+
reasons.get(item.table) ||
|
|
96
|
+
// Not attempted and no reason offered. Either the table is gone or
|
|
97
|
+
// that particular attack did not run. Said as what it is rather
|
|
98
|
+
// than folded into "fixed", because deleting a table and securing
|
|
99
|
+
// one are different acts and only one is what was asked for.
|
|
100
|
+
(tablesTouched.has(item.table)
|
|
101
|
+
? 'this particular check did not run this time'
|
|
102
|
+
: 'the table was not there to test this time'),
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
fixed.push(item);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const wasByKey = new Set(was.map(keyOf));
|
|
111
|
+
const newlyBroken = now.filter((item) => !wasByKey.has(keyOf(item)));
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
stopped: null,
|
|
115
|
+
fixed: fixed,
|
|
116
|
+
stillOpen: stillOpen,
|
|
117
|
+
unverifiable: unverifiable,
|
|
118
|
+
newlyBroken: newlyBroken,
|
|
119
|
+
// Green is the strictest thing this program says, so it is the hardest to
|
|
120
|
+
// earn: everything that was wrong is now provably right, nothing new broke,
|
|
121
|
+
// and nothing at all was left untested.
|
|
122
|
+
//
|
|
123
|
+
// Every old finding lands in exactly one of fixed / stillOpen /
|
|
124
|
+
// unverifiable, so "the other two are empty" already means "all of them
|
|
125
|
+
// were fixed" - stating that a third time as a count only made each guard
|
|
126
|
+
// able to cover for the others, which is how a broken guard stays hidden.
|
|
127
|
+
allClear:
|
|
128
|
+
!stillOpen.length &&
|
|
129
|
+
!unverifiable.length &&
|
|
130
|
+
!newlyBroken.length &&
|
|
131
|
+
!((after && after.notChecked) || []).length,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The badge, which only a genuinely clean re-check is allowed to produce. */
|
|
136
|
+
function badgeLines(result, attacksRun) {
|
|
137
|
+
if (!result.allClear) return [];
|
|
138
|
+
const when = new Date().toISOString().slice(0, 10);
|
|
139
|
+
return [
|
|
140
|
+
'',
|
|
141
|
+
' [ Kryptheon Verified ] ' + attacksRun + ' checks passed ' + when,
|
|
142
|
+
'',
|
|
143
|
+
' Every problem I found is closed, and I proved it by running the same',
|
|
144
|
+
' attacks again. Nothing was left untested.',
|
|
145
|
+
'',
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** What the re-check says, in the order it matters. */
|
|
150
|
+
function describe(result) {
|
|
151
|
+
const lines = [''];
|
|
152
|
+
|
|
153
|
+
if (result.stopped) {
|
|
154
|
+
lines.push(' I could not re-check this app, so nothing is confirmed fixed.');
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push(' ' + result.stopped);
|
|
157
|
+
lines.push('');
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (result.fixed.length) {
|
|
162
|
+
lines.push(' ' + result.fixed.length + (result.fixed.length === 1 ? ' problem is' : ' problems are') + ' fixed:');
|
|
163
|
+
lines.push('');
|
|
164
|
+
for (const item of result.fixed) {
|
|
165
|
+
lines.push(' ' + item.table + ' - I ran the same attack again and it was refused.');
|
|
166
|
+
}
|
|
167
|
+
lines.push('');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (result.stillOpen.length) {
|
|
171
|
+
lines.push(' ' + result.stillOpen.length + ' still open:');
|
|
172
|
+
lines.push('');
|
|
173
|
+
for (const item of result.stillOpen) {
|
|
174
|
+
lines.push(' ' + item.table + ' - ' + item.headline);
|
|
175
|
+
}
|
|
176
|
+
lines.push('');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Deliberately not under "fixed", and deliberately not silent. This is the
|
|
180
|
+
// one a person would otherwise read as good news.
|
|
181
|
+
if (result.unverifiable.length) {
|
|
182
|
+
lines.push(' ' + result.unverifiable.length + ' I could NOT confirm:');
|
|
183
|
+
lines.push('');
|
|
184
|
+
for (const item of result.unverifiable) {
|
|
185
|
+
lines.push(' ' + item.table + ' - ' + item.why);
|
|
186
|
+
}
|
|
187
|
+
lines.push('');
|
|
188
|
+
lines.push(' These are not fixed and not broken - they are unknown. The problem');
|
|
189
|
+
lines.push(' stopped showing up, but not because the attack was refused.');
|
|
190
|
+
lines.push('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (result.newlyBroken.length) {
|
|
194
|
+
lines.push(' ' + result.newlyBroken.length + ' NEW ' +
|
|
195
|
+
(result.newlyBroken.length === 1 ? 'problem' : 'problems') + ' that were not there before:');
|
|
196
|
+
lines.push('');
|
|
197
|
+
for (const item of result.newlyBroken) {
|
|
198
|
+
lines.push(' ' + item.table + ' - ' + item.headline);
|
|
199
|
+
}
|
|
200
|
+
lines.push('');
|
|
201
|
+
lines.push(' A fix can open something else. This is why the re-check looks at the');
|
|
202
|
+
lines.push(' whole app again and not only at what it was asked about.');
|
|
203
|
+
lines.push('');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!result.fixed.length && !result.stillOpen.length && !result.unverifiable.length && !result.newlyBroken.length) {
|
|
207
|
+
lines.push(' Nothing to re-check - there was nothing open.');
|
|
208
|
+
lines.push('');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return lines;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
keyOf: keyOf,
|
|
216
|
+
compare: compare,
|
|
217
|
+
describe: describe,
|
|
218
|
+
badgeLines: badgeLines,
|
|
219
|
+
};
|