@nodatachat/guard 3.0.0 → 3.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/dist/cli.js CHANGED
@@ -26,7 +26,8 @@ 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 = "3.0.0";
29
+ const db_detect_1 = require("./db-detect");
30
+ const VERSION = "3.1.0";
30
31
  async function main() {
31
32
  const args = process.argv.slice(2);
32
33
  // ── Subcommand routing ──
@@ -44,6 +45,12 @@ async function main() {
44
45
  if (subcommand === "attest") {
45
46
  return handleAttest(args.slice(1));
46
47
  }
48
+ if (subcommand === "detect") {
49
+ const dir = args.includes("--dir") ? (0, path_1.resolve)(args[args.indexOf("--dir") + 1]) : process.cwd();
50
+ const result = (0, db_detect_1.detectDatabases)(dir);
51
+ (0, db_detect_1.printDetectionResults)(result);
52
+ return;
53
+ }
47
54
  // Parse args
48
55
  let licenseKey;
49
56
  let dbUrl;
@@ -114,12 +121,21 @@ async function main() {
114
121
  licenseKey = process.env.NDC_LICENSE || process.env.NODATA_LICENSE_KEY || process.env.NODATA_API_KEY || process.env.NDC_API_KEY;
115
122
  if (!dbUrl)
116
123
  dbUrl = process.env.DATABASE_URL;
117
- // ── Auto-detect from .env files ──
118
- // Guard reads .env files to find DATABASE_URL and license keys.
124
+ // ── Auto-detect from .env files + project analysis ──
125
+ // Guard reads .env files AND analyzes the project to find databases.
119
126
  // ALL values stay local — never sent anywhere.
120
- // If DB URL is found, Guard asks for explicit consent before connecting.
127
+ // If a DB is found, Guard asks for explicit consent before connecting.
121
128
  if (!dbUrl || !licenseKey) {
122
129
  const envResult = readEnvFiles(projectDir);
130
+ // Also run full database detection for richer results
131
+ if (!dbUrl) {
132
+ const detection = (0, db_detect_1.detectDatabases)(projectDir);
133
+ const scannableDb = detection.databases.find(d => d.scannable && d.raw_value);
134
+ if (scannableDb && !envResult.dbUrl) {
135
+ envResult.dbUrl = scannableDb.raw_value;
136
+ envResult.dbSource = `${scannableDb.source} (${scannableDb.engine}/${scannableDb.provider})`;
137
+ }
138
+ }
123
139
  if (!licenseKey && envResult.licenseKey) {
124
140
  licenseKey = envResult.licenseKey;
125
141
  if (!ciMode)
@@ -921,11 +937,13 @@ function printHelp() {
921
937
  npx nodata-guard status # Show .capsule/ status (no scan)
922
938
  npx nodata-guard diff # Compare last 2 scans
923
939
  npx nodata-guard attest --finding ID --status fixed --note "..." # Manual attestation
940
+ npx nodata-guard detect # Detect all databases in project
924
941
 
925
942
  Subcommands:
926
943
  status Show .capsule/ evidence (scores, proof, overrides) without scanning
927
944
  diff Compare the last 2 scans — score delta, issues resolved/new
928
945
  attest Manually attest a finding (saved to .capsule/overrides.json)
946
+ detect Detect all databases in your project (env, deps, config)
929
947
  --finding <id> Finding ID (e.g., PII_UNENCRYPTED_email, ROUTE_NO_AUTH)
930
948
  --status <status> fixed | accepted_risk | not_applicable | compensating_control
931
949
  --note <text> Explanation of what was done
@@ -0,0 +1,24 @@
1
+ export type DbEngine = "postgresql" | "mysql" | "mongodb" | "redis" | "sqlite" | "sqlserver" | "cockroachdb" | "turso" | "dynamodb" | "firebase" | "fauna" | "xata" | "convex" | "d1" | "unknown";
2
+ export type DbProvider = "supabase" | "neon" | "planetscale" | "upstash" | "railway" | "render" | "fly" | "vercel-postgres" | "aws-rds" | "azure" | "gcp-cloudsql" | "digitalocean" | "cockroach-cloud" | "turso-cloud" | "mongodb-atlas" | "firebase-google" | "fauna-cloud" | "xata-cloud" | "convex-cloud" | "cloudflare-d1" | "aiven" | "tembo" | "timescale" | "elephantsql" | "local" | "unknown";
3
+ export interface DetectedDatabase {
4
+ engine: DbEngine;
5
+ provider: DbProvider;
6
+ source: string;
7
+ connection_key: string;
8
+ connection_value: string;
9
+ raw_value: string;
10
+ scannable: boolean;
11
+ scan_method: string;
12
+ confidence: number;
13
+ details: string;
14
+ }
15
+ export interface DetectionResult {
16
+ databases: DetectedDatabase[];
17
+ orm_detected: string | null;
18
+ framework_detected: string | null;
19
+ env_files_scanned: string[];
20
+ config_files_found: string[];
21
+ total_signals: number;
22
+ }
23
+ export declare function detectDatabases(projectDir: string): DetectionResult;
24
+ export declare function printDetectionResults(result: DetectionResult): void;
@@ -0,0 +1,443 @@
1
+ "use strict";
2
+ // ═══════════════════════════════════════════════════════════
3
+ // @nodatachat/guard — Universal Database Detector
4
+ //
5
+ // Scans a project to find ALL database connections:
6
+ // 1. .env files — connection strings, URLs, keys
7
+ // 2. package.json — ORM/driver dependencies
8
+ // 3. Config files — prisma.schema, drizzle.config, knexfile, etc.
9
+ // 4. Code patterns — import statements, connection code
10
+ //
11
+ // Supports: PostgreSQL, MySQL, MongoDB, Redis, SQLite,
12
+ // SQL Server, CockroachDB, Turso/LibSQL, DynamoDB,
13
+ // Supabase, Neon, PlanetScale, Upstash, Firebase,
14
+ // Fauna, Xata, Convex, D1 (Cloudflare)
15
+ //
16
+ // ALL detection is LOCAL. No data leaves the machine.
17
+ // ═══════════════════════════════════════════════════════════
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.detectDatabases = detectDatabases;
20
+ exports.printDetectionResults = printDetectionResults;
21
+ const path_1 = require("path");
22
+ const fs_1 = require("fs");
23
+ const ENV_PATTERNS = [
24
+ // Supabase
25
+ { keys: ["NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_URL", "VITE_SUPABASE_URL", "REACT_APP_SUPABASE_URL", "NUXT_PUBLIC_SUPABASE_URL"],
26
+ engine: "postgresql", provider: "supabase", urlPattern: /supabase\.co/ },
27
+ { keys: ["SUPABASE_DB_URL"], engine: "postgresql", provider: "supabase" },
28
+ // Neon
29
+ { keys: ["DATABASE_URL", "POSTGRES_URL", "NEON_DATABASE_URL"],
30
+ engine: "postgresql", provider: "neon", urlPattern: /neon\.tech/ },
31
+ // PlanetScale
32
+ { keys: ["DATABASE_URL"], engine: "mysql", provider: "planetscale", urlPattern: /planetscale|psdb\.cloud/ },
33
+ // Vercel Postgres
34
+ { keys: ["POSTGRES_URL", "POSTGRES_PRISMA_URL", "POSTGRES_URL_NON_POOLING"],
35
+ engine: "postgresql", provider: "vercel-postgres", urlPattern: /vercel-storage\.com/ },
36
+ // Railway
37
+ { keys: ["DATABASE_URL", "RAILWAY_DATABASE_URL"],
38
+ engine: "postgresql", provider: "railway", urlPattern: /railway\.app/ },
39
+ // Render
40
+ { keys: ["DATABASE_URL"], engine: "postgresql", provider: "render", urlPattern: /render\.com/ },
41
+ // Fly.io
42
+ { keys: ["DATABASE_URL"], engine: "postgresql", provider: "fly", urlPattern: /fly\.dev|flycast/ },
43
+ // Upstash
44
+ { keys: ["UPSTASH_REDIS_REST_URL", "KV_REST_API_URL"],
45
+ engine: "redis", provider: "upstash" },
46
+ // MongoDB Atlas
47
+ { keys: ["MONGODB_URI", "MONGO_URL", "MONGODB_URL", "DATABASE_URL"],
48
+ engine: "mongodb", provider: "mongodb-atlas", urlPattern: /mongodb\.net|mongodb\+srv/ },
49
+ // AWS RDS
50
+ { keys: ["DATABASE_URL", "RDS_HOSTNAME", "RDS_DB_NAME"],
51
+ engine: "postgresql", provider: "aws-rds", urlPattern: /rds\.amazonaws\.com/ },
52
+ // Azure
53
+ { keys: ["DATABASE_URL", "AZURE_SQL_CONNECTION_STRING"],
54
+ engine: "sqlserver", provider: "azure", urlPattern: /database\.windows\.net|azure/ },
55
+ // GCP Cloud SQL
56
+ { keys: ["DATABASE_URL", "CLOUD_SQL_CONNECTION_NAME"],
57
+ engine: "postgresql", provider: "gcp-cloudsql", urlPattern: /cloudsql|google/ },
58
+ // CockroachDB
59
+ { keys: ["DATABASE_URL"], engine: "cockroachdb", provider: "cockroach-cloud", urlPattern: /cockroachlabs\.cloud/ },
60
+ // Turso
61
+ { keys: ["TURSO_DATABASE_URL", "TURSO_AUTH_TOKEN"],
62
+ engine: "turso", provider: "turso-cloud", urlPattern: /turso\.io|libsql/ },
63
+ // Firebase
64
+ { keys: ["FIREBASE_DATABASE_URL", "NEXT_PUBLIC_FIREBASE_DATABASE_URL"],
65
+ engine: "firebase", provider: "firebase-google", urlPattern: /firebaseio\.com/ },
66
+ // Fauna
67
+ { keys: ["FAUNA_SECRET", "FAUNADB_SECRET"],
68
+ engine: "fauna", provider: "fauna-cloud" },
69
+ // Xata
70
+ { keys: ["XATA_API_KEY", "XATA_DATABASE_URL"],
71
+ engine: "xata", provider: "xata-cloud" },
72
+ // Convex
73
+ { keys: ["CONVEX_DEPLOYMENT", "NEXT_PUBLIC_CONVEX_URL"],
74
+ engine: "convex", provider: "convex-cloud" },
75
+ // Aiven
76
+ { keys: ["DATABASE_URL"], engine: "postgresql", provider: "aiven", urlPattern: /aivencloud\.com/ },
77
+ // Tembo
78
+ { keys: ["DATABASE_URL"], engine: "postgresql", provider: "tembo", urlPattern: /tembo\.io/ },
79
+ // Timescale
80
+ { keys: ["DATABASE_URL"], engine: "postgresql", provider: "timescale", urlPattern: /timescaledb\.io|tsdb\.cloud/ },
81
+ // DigitalOcean
82
+ { keys: ["DATABASE_URL"], engine: "postgresql", provider: "digitalocean", urlPattern: /db\.ondigitalocean\.com/ },
83
+ // Generic PostgreSQL
84
+ { keys: ["DATABASE_URL", "DIRECT_URL", "POSTGRES_URL", "PG_CONNECTION_STRING", "DB_URL", "POSTGRES_PRISMA_URL"],
85
+ engine: "postgresql", provider: "unknown", urlPattern: /^postgres(ql)?:\/\// },
86
+ // Generic MySQL
87
+ { keys: ["DATABASE_URL", "MYSQL_URL", "DB_URL"],
88
+ engine: "mysql", provider: "unknown", urlPattern: /^mysql:\/\// },
89
+ // Generic Redis
90
+ { keys: ["REDIS_URL", "REDIS_TLS_URL"],
91
+ engine: "redis", provider: "unknown", urlPattern: /^redis(s)?:\/\// },
92
+ // Generic SQLite
93
+ { keys: ["DATABASE_URL"], engine: "sqlite", provider: "local", urlPattern: /^file:|\.sqlite|\.db$/ },
94
+ // Generic SQL Server
95
+ { keys: ["DATABASE_URL", "MSSQL_CONNECTION_STRING"],
96
+ engine: "sqlserver", provider: "unknown", urlPattern: /^mssql:\/\/|Server=/ },
97
+ ];
98
+ const DEP_SIGNALS = [
99
+ // ORMs (high confidence — they define the DB)
100
+ { packages: ["@prisma/client", "prisma"], engine: "postgresql", orm: "prisma", confidence: 90 },
101
+ { packages: ["drizzle-orm"], engine: "postgresql", orm: "drizzle", confidence: 85 },
102
+ { packages: ["typeorm"], engine: "postgresql", orm: "typeorm", confidence: 80 },
103
+ { packages: ["knex"], engine: "postgresql", orm: "knex", confidence: 75 },
104
+ { packages: ["sequelize"], engine: "postgresql", orm: "sequelize", confidence: 75 },
105
+ { packages: ["mongoose", "mongodb"], engine: "mongodb", orm: "mongoose", confidence: 90 },
106
+ { packages: ["@libsql/client", "better-sqlite3"], engine: "sqlite", orm: undefined, confidence: 85 },
107
+ // Direct drivers
108
+ { packages: ["pg", "@neondatabase/serverless"], engine: "postgresql", confidence: 95 },
109
+ { packages: ["mysql2", "mysql"], engine: "mysql", confidence: 95 },
110
+ { packages: ["redis", "ioredis", "@upstash/redis"], engine: "redis", confidence: 90 },
111
+ { packages: ["tedious", "mssql"], engine: "sqlserver", confidence: 90 },
112
+ // Managed DB SDKs
113
+ { packages: ["@supabase/supabase-js"], engine: "postgresql", confidence: 85 },
114
+ { packages: ["@planetscale/database"], engine: "mysql", confidence: 90 },
115
+ { packages: ["@vercel/postgres"], engine: "postgresql", confidence: 90 },
116
+ { packages: ["@neondatabase/serverless"], engine: "postgresql", confidence: 90 },
117
+ { packages: ["@libsql/client"], engine: "turso", confidence: 90 },
118
+ { packages: ["firebase", "firebase-admin"], engine: "firebase", confidence: 80 },
119
+ { packages: ["faunadb", "fauna"], engine: "fauna", confidence: 90 },
120
+ { packages: ["@xata.io/client"], engine: "xata", confidence: 90 },
121
+ { packages: ["convex"], engine: "convex", confidence: 85 },
122
+ { packages: ["@aws-sdk/client-dynamodb"], engine: "dynamodb", confidence: 90 },
123
+ ];
124
+ const CONFIG_SIGNALS = [
125
+ {
126
+ files: ["prisma/schema.prisma", "schema.prisma"],
127
+ engine: "postgresql", orm: "prisma",
128
+ parseProvider: (content) => {
129
+ if (content.includes("supabase"))
130
+ return "supabase";
131
+ if (content.includes("neon.tech"))
132
+ return "neon";
133
+ if (content.includes("planetscale"))
134
+ return "planetscale";
135
+ if (content.includes("cockroach"))
136
+ return "cockroach-cloud";
137
+ if (/provider\s*=\s*"mysql"/i.test(content))
138
+ return "unknown"; // engine override
139
+ if (/provider\s*=\s*"sqlite"/i.test(content))
140
+ return "local";
141
+ return "unknown";
142
+ },
143
+ },
144
+ {
145
+ files: ["drizzle.config.ts", "drizzle.config.js", "drizzle.config.mjs"],
146
+ engine: "postgresql", orm: "drizzle",
147
+ },
148
+ {
149
+ files: ["knexfile.js", "knexfile.ts"],
150
+ engine: "postgresql", orm: "knex",
151
+ },
152
+ {
153
+ files: ["ormconfig.json", "ormconfig.ts", "ormconfig.js"],
154
+ engine: "postgresql", orm: "typeorm",
155
+ },
156
+ {
157
+ files: [".sequelizerc", "config/database.js", "config/database.json"],
158
+ engine: "postgresql", orm: "sequelize",
159
+ },
160
+ {
161
+ files: ["firebase.json", ".firebaserc"],
162
+ engine: "firebase",
163
+ },
164
+ {
165
+ files: ["convex/_generated", "convex/schema.ts"],
166
+ engine: "convex",
167
+ },
168
+ {
169
+ files: ["wrangler.toml"], // D1
170
+ engine: "d1",
171
+ parseProvider: (content) => content.includes("d1_databases") ? "cloudflare-d1" : null,
172
+ },
173
+ ];
174
+ // ── Engine scan capabilities ──
175
+ const SCAN_SUPPORT = {
176
+ postgresql: { scannable: true, method: "direct_sql" },
177
+ mysql: { scannable: true, method: "direct_sql" },
178
+ cockroachdb: { scannable: true, method: "direct_sql" },
179
+ sqlserver: { scannable: false, method: "not_yet_supported" },
180
+ mongodb: { scannable: false, method: "not_yet_supported" },
181
+ redis: { scannable: false, method: "not_applicable" },
182
+ sqlite: { scannable: false, method: "not_yet_supported" },
183
+ turso: { scannable: false, method: "not_yet_supported" },
184
+ dynamodb: { scannable: false, method: "not_applicable" },
185
+ firebase: { scannable: false, method: "not_applicable" },
186
+ fauna: { scannable: false, method: "not_applicable" },
187
+ xata: { scannable: false, method: "not_yet_supported" },
188
+ convex: { scannable: false, method: "not_applicable" },
189
+ d1: { scannable: false, method: "not_yet_supported" },
190
+ unknown: { scannable: false, method: "unknown" },
191
+ };
192
+ // ── Mask connection string ──
193
+ function maskValue(val) {
194
+ if (val.includes("://")) {
195
+ try {
196
+ const u = new URL(val);
197
+ return `${u.protocol}//${u.username ? u.username.slice(0, 4) + "***" : "***"}@${u.hostname}:${u.port || "???"}${u.pathname}`;
198
+ }
199
+ catch { /* fallback */ }
200
+ }
201
+ if (val.length > 20)
202
+ return val.slice(0, 8) + "..." + val.slice(-4);
203
+ return val.slice(0, 4) + "***";
204
+ }
205
+ // ── Main detection ──
206
+ function detectDatabases(projectDir) {
207
+ const databases = [];
208
+ const envFilesScanned = [];
209
+ const configFilesFound = [];
210
+ let ormDetected = null;
211
+ let frameworkDetected = null;
212
+ const seen = new Set(); // dedup by connection_key + engine
213
+ // ── Phase 1: Scan .env files ──
214
+ const envFiles = [
215
+ ".env", ".env.local", ".env.development", ".env.development.local",
216
+ ".env.production", ".env.production.local", ".env.staging",
217
+ ];
218
+ const envVars = new Map();
219
+ for (const envFile of envFiles) {
220
+ const filePath = (0, path_1.resolve)(projectDir, envFile);
221
+ if (!(0, fs_1.existsSync)(filePath))
222
+ continue;
223
+ envFilesScanned.push(envFile);
224
+ try {
225
+ const content = (0, fs_1.readFileSync)(filePath, "utf-8");
226
+ for (const line of content.split("\n")) {
227
+ const trimmed = line.trim();
228
+ if (!trimmed || trimmed.startsWith("#"))
229
+ continue;
230
+ const eqIdx = trimmed.indexOf("=");
231
+ if (eqIdx < 1)
232
+ continue;
233
+ const key = trimmed.slice(0, eqIdx).trim();
234
+ let val = trimmed.slice(eqIdx + 1).trim();
235
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
236
+ val = val.slice(1, -1);
237
+ }
238
+ if (val)
239
+ envVars.set(key, { val, source: envFile });
240
+ }
241
+ }
242
+ catch { /* skip */ }
243
+ }
244
+ // Match env vars to patterns
245
+ for (const pattern of ENV_PATTERNS) {
246
+ for (const key of pattern.keys) {
247
+ const entry = envVars.get(key);
248
+ if (!entry)
249
+ continue;
250
+ // Check URL pattern if specified
251
+ if (pattern.urlPattern && !pattern.urlPattern.test(entry.val))
252
+ continue;
253
+ const dedup = `${key}:${pattern.engine}:${pattern.provider}`;
254
+ if (seen.has(dedup))
255
+ continue;
256
+ seen.add(dedup);
257
+ const support = SCAN_SUPPORT[pattern.engine];
258
+ databases.push({
259
+ engine: pattern.engine,
260
+ provider: pattern.provider,
261
+ source: entry.source,
262
+ connection_key: key,
263
+ connection_value: maskValue(entry.val),
264
+ raw_value: entry.val,
265
+ scannable: support.scannable,
266
+ scan_method: support.method,
267
+ confidence: 95,
268
+ details: `Found ${key} in ${entry.source} → ${pattern.engine} (${pattern.provider})`,
269
+ });
270
+ }
271
+ }
272
+ // ── Phase 2: Scan package.json ──
273
+ const pkgPath = (0, path_1.resolve)(projectDir, "package.json");
274
+ if ((0, fs_1.existsSync)(pkgPath)) {
275
+ try {
276
+ const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, "utf-8"));
277
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
278
+ // Framework detection
279
+ if (allDeps.next)
280
+ frameworkDetected = "nextjs";
281
+ else if (allDeps.nuxt)
282
+ frameworkDetected = "nuxt";
283
+ else if (allDeps.express)
284
+ frameworkDetected = "express";
285
+ else if (allDeps.fastify)
286
+ frameworkDetected = "fastify";
287
+ else if (allDeps["@sveltejs/kit"])
288
+ frameworkDetected = "sveltekit";
289
+ else if (allDeps["@angular/core"])
290
+ frameworkDetected = "angular";
291
+ for (const signal of DEP_SIGNALS) {
292
+ const found = signal.packages.find(p => allDeps[p]);
293
+ if (!found)
294
+ continue;
295
+ if (signal.orm && !ormDetected)
296
+ ormDetected = signal.orm;
297
+ const dedup = `dep:${found}:${signal.engine}`;
298
+ if (seen.has(dedup))
299
+ continue;
300
+ seen.add(dedup);
301
+ const support = SCAN_SUPPORT[signal.engine];
302
+ databases.push({
303
+ engine: signal.engine,
304
+ provider: "unknown",
305
+ source: "package.json",
306
+ connection_key: found,
307
+ connection_value: allDeps[found],
308
+ raw_value: "",
309
+ scannable: support.scannable,
310
+ scan_method: support.method,
311
+ confidence: signal.confidence,
312
+ details: `Dependency "${found}" detected → ${signal.engine}${signal.orm ? ` (ORM: ${signal.orm})` : ""}`,
313
+ });
314
+ }
315
+ }
316
+ catch { /* skip */ }
317
+ }
318
+ // Also check apps/*/package.json for monorepos
319
+ for (const subDir of ["apps/web", "apps/api", "apps/server", "packages/db", "packages/database"]) {
320
+ const subPkg = (0, path_1.resolve)(projectDir, subDir, "package.json");
321
+ if (!(0, fs_1.existsSync)(subPkg))
322
+ continue;
323
+ try {
324
+ const pkg = JSON.parse((0, fs_1.readFileSync)(subPkg, "utf-8"));
325
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
326
+ for (const signal of DEP_SIGNALS) {
327
+ const found = signal.packages.find(p => allDeps[p]);
328
+ if (!found)
329
+ continue;
330
+ const dedup = `dep:${subDir}:${found}:${signal.engine}`;
331
+ if (seen.has(dedup))
332
+ continue;
333
+ seen.add(dedup);
334
+ if (signal.orm && !ormDetected)
335
+ ormDetected = signal.orm;
336
+ const support = SCAN_SUPPORT[signal.engine];
337
+ databases.push({
338
+ engine: signal.engine, provider: "unknown",
339
+ source: `${subDir}/package.json`,
340
+ connection_key: found, connection_value: allDeps[found], raw_value: "",
341
+ scannable: support.scannable, scan_method: support.method,
342
+ confidence: signal.confidence,
343
+ details: `Dependency "${found}" in ${subDir} → ${signal.engine}`,
344
+ });
345
+ }
346
+ }
347
+ catch { /* skip */ }
348
+ }
349
+ // ── Phase 3: Scan config files ──
350
+ for (const signal of CONFIG_SIGNALS) {
351
+ for (const file of signal.files) {
352
+ const filePath = (0, path_1.resolve)(projectDir, file);
353
+ if (!(0, fs_1.existsSync)(filePath))
354
+ continue;
355
+ configFilesFound.push(file);
356
+ const dedup = `config:${file}:${signal.engine}`;
357
+ if (seen.has(dedup))
358
+ continue;
359
+ seen.add(dedup);
360
+ let provider = "unknown";
361
+ try {
362
+ const content = (0, fs_1.readFileSync)(filePath, "utf-8");
363
+ if (signal.parseProvider) {
364
+ provider = signal.parseProvider(content) || "unknown";
365
+ }
366
+ }
367
+ catch { /* skip */ }
368
+ if (signal.orm && !ormDetected)
369
+ ormDetected = signal.orm;
370
+ const support = SCAN_SUPPORT[signal.engine];
371
+ databases.push({
372
+ engine: signal.engine, provider,
373
+ source: file, connection_key: file, connection_value: "", raw_value: "",
374
+ scannable: support.scannable, scan_method: support.method,
375
+ confidence: 80,
376
+ details: `Config file "${file}" detected → ${signal.engine}${signal.orm ? ` (ORM: ${signal.orm})` : ""}`,
377
+ });
378
+ }
379
+ }
380
+ // ── Deduplicate: prefer env vars over deps, higher confidence wins ──
381
+ const unique = new Map();
382
+ for (const db of databases) {
383
+ const key = `${db.engine}:${db.provider !== "unknown" ? db.provider : db.connection_key}`;
384
+ const existing = unique.get(key);
385
+ if (!existing || db.confidence > existing.confidence || (db.raw_value && !existing.raw_value)) {
386
+ unique.set(key, db);
387
+ }
388
+ }
389
+ return {
390
+ databases: [...unique.values()].sort((a, b) => b.confidence - a.confidence),
391
+ orm_detected: ormDetected,
392
+ framework_detected: frameworkDetected,
393
+ env_files_scanned: envFilesScanned,
394
+ config_files_found: configFilesFound,
395
+ total_signals: databases.length,
396
+ };
397
+ }
398
+ // ── Pretty print detection results ──
399
+ function printDetectionResults(result) {
400
+ console.log("");
401
+ console.log(" ╔══════════════════════════════════════════╗");
402
+ console.log(" ║ \x1b[33mDatabase Detection Results\x1b[0m ║");
403
+ console.log(" ╚══════════════════════════════════════════╝");
404
+ console.log("");
405
+ if (result.framework_detected) {
406
+ console.log(` Framework: \x1b[36m${result.framework_detected}\x1b[0m`);
407
+ }
408
+ if (result.orm_detected) {
409
+ console.log(` ORM: \x1b[36m${result.orm_detected}\x1b[0m`);
410
+ }
411
+ console.log(` Scanned: ${result.env_files_scanned.length} env files, ${result.config_files_found.length} config files`);
412
+ console.log(` Signals: ${result.total_signals} total`);
413
+ console.log("");
414
+ if (result.databases.length === 0) {
415
+ console.log(" \x1b[33mNo databases detected.\x1b[0m");
416
+ console.log(" Add DATABASE_URL to .env.local or install a database driver.\n");
417
+ return;
418
+ }
419
+ console.log(` Found \x1b[1m${result.databases.length}\x1b[0m database(s):\n`);
420
+ for (let i = 0; i < result.databases.length; i++) {
421
+ const db = result.databases[i];
422
+ const num = ` ${i + 1}.`;
423
+ const engineColor = db.scannable ? "\x1b[32m" : "\x1b[33m";
424
+ const scanLabel = db.scannable ? "\x1b[32m✓ scannable\x1b[0m" : `\x1b[33m○ ${db.scan_method}\x1b[0m`;
425
+ console.log(`${num} ${engineColor}${db.engine.toUpperCase()}\x1b[0m — ${db.provider} ${scanLabel}`);
426
+ console.log(` Source: ${db.source}`);
427
+ if (db.connection_value) {
428
+ console.log(` Connection: ${db.connection_value}`);
429
+ }
430
+ console.log(` Confidence: ${db.confidence}%`);
431
+ console.log(` ${db.details}`);
432
+ console.log("");
433
+ }
434
+ const scannable = result.databases.filter(d => d.scannable);
435
+ if (scannable.length > 0) {
436
+ console.log(` \x1b[32m${scannable.length} database(s) can be scanned for encryption.\x1b[0m`);
437
+ }
438
+ const notScannable = result.databases.filter(d => !d.scannable);
439
+ if (notScannable.length > 0) {
440
+ console.log(` \x1b[33m${notScannable.length} database(s) not yet supported for scanning.\x1b[0m`);
441
+ }
442
+ console.log("");
443
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodatachat/guard",
3
- "version": "3.0.0",
3
+ "version": "3.1.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",