@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 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.5.0";
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
- if (!dbUrl && envResult.dbUrl) {
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(envResult.dbUrl);
158
+ const masked = maskDbUrl(detectedDbUrl);
131
159
  if (ciMode) {
132
160
  // CI mode: auto-consent (operator set up the env)
133
- dbUrl = envResult.dbUrl;
134
- console.log(`[nodata-guard] Using database from ${envResult.dbSource}`);
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: ${envResult.dbSource}`);
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 = envResult.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
- const a = answer.trim().toLowerCase();
653
- resolve(a === "y" || a === "yes" || a === "כ" || a === "כן");
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
  }
@@ -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+/=]+$') as base64_long
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
- const encCount = aesGcm + encV1 + ndcEnc + b64;
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
- : ndcEnc > 0 ? "ndc_enc (Protect)"
134
- : b64 > 0 ? "base64_long"
135
- : "unknown";
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
- if (encCount > 0 && encCount >= nonNull * 0.9) {
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.5.0",
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",