@nodatachat/guard 2.5.0 → 2.6.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/dist/cli.js +43 -10
- package/dist/db-scanner.js +21 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ const reporter_1 = require("./reporter");
|
|
|
26
26
|
const scheduler_1 = require("./fixers/scheduler");
|
|
27
27
|
const vault_crypto_1 = require("./vault-crypto");
|
|
28
28
|
const capsule_dir_1 = require("./capsule-dir");
|
|
29
|
-
const VERSION = "2.
|
|
29
|
+
const VERSION = "2.6.0";
|
|
30
30
|
async function main() {
|
|
31
31
|
const args = process.argv.slice(2);
|
|
32
32
|
// ── Subcommand routing ──
|
|
@@ -125,13 +125,41 @@ async function main() {
|
|
|
125
125
|
if (!ciMode)
|
|
126
126
|
console.log(`\n \x1b[32m✓\x1b[0m Found license key in ${envResult.licenseSource}`);
|
|
127
127
|
}
|
|
128
|
-
|
|
128
|
+
// Try direct DB URL first
|
|
129
|
+
let detectedDbUrl = envResult.dbUrl;
|
|
130
|
+
let detectedDbSource = envResult.dbSource;
|
|
131
|
+
// If no direct DB URL found, try to construct from Supabase URL
|
|
132
|
+
// Supabase projects have a predictable connection string format
|
|
133
|
+
if (!detectedDbUrl && envResult.supabaseUrl && !ciMode) {
|
|
134
|
+
const match = envResult.supabaseUrl.match(/https:\/\/([a-z0-9]+)\.supabase\.co/);
|
|
135
|
+
if (match) {
|
|
136
|
+
const projectRef = match[1];
|
|
137
|
+
console.log("");
|
|
138
|
+
console.log(" ╔══════════════════════════════════════════╗");
|
|
139
|
+
console.log(" ║ \x1b[33mSupabase project detected\x1b[0m ║");
|
|
140
|
+
console.log(" ╚══════════════════════════════════════════╝");
|
|
141
|
+
console.log(` Project: ${projectRef}.supabase.co`);
|
|
142
|
+
console.log("");
|
|
143
|
+
console.log(" \x1b[2mTo verify DB encryption, Guard needs a direct connection.\x1b[0m");
|
|
144
|
+
console.log(" \x1b[2mFind it in: Supabase Dashboard → Settings → Database → URI\x1b[0m");
|
|
145
|
+
console.log("");
|
|
146
|
+
const password = await askConsent(" Paste your database password (or press Enter to skip): ", true);
|
|
147
|
+
if (password && typeof password === "string" && password.trim()) {
|
|
148
|
+
detectedDbUrl = `postgresql://postgres.${projectRef}:${password.trim()}@aws-0-eu-central-1.pooler.supabase.com:6543/postgres`;
|
|
149
|
+
detectedDbSource = "Supabase (auto-constructed)";
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.log(" \x1b[33m→\x1b[0m No password — skipping database scan\n");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!dbUrl && detectedDbUrl) {
|
|
129
157
|
// Mask the connection string for display (show host only)
|
|
130
|
-
const masked = maskDbUrl(
|
|
158
|
+
const masked = maskDbUrl(detectedDbUrl);
|
|
131
159
|
if (ciMode) {
|
|
132
160
|
// CI mode: auto-consent (operator set up the env)
|
|
133
|
-
dbUrl =
|
|
134
|
-
console.log(`[nodata-guard] Using database from ${
|
|
161
|
+
dbUrl = detectedDbUrl;
|
|
162
|
+
console.log(`[nodata-guard] Using database from ${detectedDbSource}`);
|
|
135
163
|
}
|
|
136
164
|
else {
|
|
137
165
|
// Interactive: ask for consent
|
|
@@ -139,7 +167,7 @@ async function main() {
|
|
|
139
167
|
console.log(" ╔══════════════════════════════════════════╗");
|
|
140
168
|
console.log(" ║ \x1b[33mDatabase connection found\x1b[0m ║");
|
|
141
169
|
console.log(" ╚══════════════════════════════════════════╝");
|
|
142
|
-
console.log(` Source: ${
|
|
170
|
+
console.log(` Source: ${detectedDbSource}`);
|
|
143
171
|
console.log(` URL: ${masked}`);
|
|
144
172
|
console.log("");
|
|
145
173
|
console.log(" \x1b[2mGuard will connect to your database to verify encryption.\x1b[0m");
|
|
@@ -148,7 +176,7 @@ async function main() {
|
|
|
148
176
|
console.log("");
|
|
149
177
|
const consent = await askConsent(" Connect to database? (y/n): ");
|
|
150
178
|
if (consent) {
|
|
151
|
-
dbUrl =
|
|
179
|
+
dbUrl = detectedDbUrl;
|
|
152
180
|
console.log(` \x1b[32m✓\x1b[0m Database scan enabled\n`);
|
|
153
181
|
}
|
|
154
182
|
else {
|
|
@@ -643,14 +671,19 @@ function maskDbUrl(url) {
|
|
|
643
671
|
return url.slice(0, 20) + "...****";
|
|
644
672
|
}
|
|
645
673
|
}
|
|
646
|
-
function askConsent(prompt) {
|
|
674
|
+
function askConsent(prompt, returnRaw) {
|
|
647
675
|
return new Promise((resolve) => {
|
|
648
676
|
const readline = require("readline");
|
|
649
677
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
650
678
|
rl.question(prompt, (answer) => {
|
|
651
679
|
rl.close();
|
|
652
|
-
|
|
653
|
-
|
|
680
|
+
if (returnRaw) {
|
|
681
|
+
resolve(answer);
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
const a = answer.trim().toLowerCase();
|
|
685
|
+
resolve(a === "y" || a === "yes" || a === "כ" || a === "כן");
|
|
686
|
+
}
|
|
654
687
|
});
|
|
655
688
|
});
|
|
656
689
|
}
|
package/dist/db-scanner.js
CHANGED
|
@@ -109,30 +109,38 @@ async function scanDatabase(connectionString, onProgress) {
|
|
|
109
109
|
// aes256gcm:v1: — legacy direct encryption (13 chars)
|
|
110
110
|
// enc:v1: — Capsule Proxy encryption (7 chars)
|
|
111
111
|
// ndc_enc_ — @nodatachat/protect format (8 chars)
|
|
112
|
+
// enc: — any enc: prefix variant (4 chars)
|
|
113
|
+
// Also checks for long base64 strings (>80 chars, alphanumeric)
|
|
112
114
|
const { rows } = await client.query(`
|
|
113
115
|
SELECT
|
|
114
116
|
count(*) as total,
|
|
115
117
|
count(${quoteIdent(col)}) as non_null,
|
|
116
118
|
count(*) FILTER (WHERE LEFT(${quoteIdent(col)}::text, 13) = 'aes256gcm:v1:') as aes_gcm,
|
|
117
119
|
count(*) FILTER (WHERE LEFT(${quoteIdent(col)}::text, 7) = 'enc:v1:') as enc_v1,
|
|
120
|
+
count(*) FILTER (WHERE LEFT(${quoteIdent(col)}::text, 4) = 'enc:') as enc_any,
|
|
118
121
|
count(*) FILTER (WHERE LEFT(${quoteIdent(col)}::text, 8) = 'ndc_enc_') as ndc_enc,
|
|
119
122
|
count(*) FILTER (WHERE LENGTH(${quoteIdent(col)}::text) > 80
|
|
120
|
-
AND ${quoteIdent(col)}::text ~ '^[A-Za-z0-9
|
|
123
|
+
AND ${quoteIdent(col)}::text ~ '^[A-Za-z0-9+/=:_-]+$') as base64_long
|
|
121
124
|
FROM ${quoteIdent(field.table)}
|
|
122
125
|
`);
|
|
123
126
|
const total = parseInt(rows[0].total);
|
|
124
127
|
const nonNull = parseInt(rows[0].non_null);
|
|
125
128
|
const aesGcm = parseInt(rows[0].aes_gcm);
|
|
126
129
|
const encV1 = parseInt(rows[0].enc_v1);
|
|
130
|
+
const encAny = parseInt(rows[0].enc_any);
|
|
127
131
|
const ndcEnc = parseInt(rows[0].ndc_enc);
|
|
128
132
|
const b64 = parseInt(rows[0].base64_long);
|
|
129
|
-
|
|
133
|
+
// enc_any catches enc:v1:, enc:v2:, enc:aes:, etc.
|
|
134
|
+
// Use the most specific match for naming, but count all enc: variants
|
|
135
|
+
const encDirectPrefixes = Math.max(aesGcm, encV1, encAny, ndcEnc);
|
|
136
|
+
const encCount = encDirectPrefixes + (encDirectPrefixes === 0 ? b64 : 0);
|
|
130
137
|
// Determine pattern name for reporting
|
|
131
138
|
const pattern = aesGcm > 0 ? "aes256gcm:v1"
|
|
132
139
|
: encV1 > 0 ? "enc:v1 (Capsule Proxy)"
|
|
133
|
-
:
|
|
134
|
-
:
|
|
135
|
-
: "
|
|
140
|
+
: encAny > 0 ? "enc: (encrypted prefix)"
|
|
141
|
+
: ndcEnc > 0 ? "ndc_enc (Protect)"
|
|
142
|
+
: b64 > 0 ? "base64_long"
|
|
143
|
+
: "unknown";
|
|
136
144
|
if (isCompanion && encCount > 0) {
|
|
137
145
|
field.encrypted = true;
|
|
138
146
|
field.encryption_pattern = pattern;
|
|
@@ -140,10 +148,17 @@ async function scanDatabase(connectionString, onProgress) {
|
|
|
140
148
|
}
|
|
141
149
|
else if (!isCompanion) {
|
|
142
150
|
field.row_count = total;
|
|
143
|
-
|
|
151
|
+
// Mark as encrypted if:
|
|
152
|
+
// - At least 1 row has an encryption prefix, AND
|
|
153
|
+
// - At least 30% of non-null rows are encrypted (threshold for partial encryption)
|
|
154
|
+
// OR at least 1 row has encryption AND total rows < 10 (small table — any encryption counts)
|
|
155
|
+
const threshold = nonNull > 0 ? encCount / nonNull : 0;
|
|
156
|
+
if (encCount > 0 && (threshold >= 0.3 || (nonNull < 10 && encCount > 0))) {
|
|
144
157
|
field.encrypted = true;
|
|
145
158
|
field.encryption_pattern = pattern;
|
|
146
159
|
field.encrypted_count = encCount;
|
|
160
|
+
// Also record the sentinel prefix for proof.json
|
|
161
|
+
field.sentinel_prefix = pattern;
|
|
147
162
|
}
|
|
148
163
|
}
|
|
149
164
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodatachat/guard",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "NoData Guard — continuous security scanner. Runs locally, reports only metadata. Your data never leaves your machine.",
|
|
5
5
|
"main": "./dist/cli.js",
|
|
6
6
|
"types": "./dist/cli.d.ts",
|