@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 +22 -4
- package/dist/db-detect.d.ts +24 -0
- package/dist/db-detect.js +443 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|