@nodatachat/guard 2.0.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/LICENSE.md +28 -0
- package/README.md +120 -0
- package/dist/activation.d.ts +8 -0
- package/dist/activation.js +110 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +458 -0
- package/dist/code-scanner.d.ts +14 -0
- package/dist/code-scanner.js +309 -0
- package/dist/db-scanner.d.ts +7 -0
- package/dist/db-scanner.js +185 -0
- package/dist/fixers/fix-csrf.d.ts +9 -0
- package/dist/fixers/fix-csrf.js +113 -0
- package/dist/fixers/fix-gitignore.d.ts +9 -0
- package/dist/fixers/fix-gitignore.js +71 -0
- package/dist/fixers/fix-headers.d.ts +9 -0
- package/dist/fixers/fix-headers.js +118 -0
- package/dist/fixers/fix-pii-encrypt.d.ts +9 -0
- package/dist/fixers/fix-pii-encrypt.js +298 -0
- package/dist/fixers/fix-rate-limit.d.ts +9 -0
- package/dist/fixers/fix-rate-limit.js +102 -0
- package/dist/fixers/fix-rls.d.ts +9 -0
- package/dist/fixers/fix-rls.js +243 -0
- package/dist/fixers/fix-routes-auth.d.ts +9 -0
- package/dist/fixers/fix-routes-auth.js +82 -0
- package/dist/fixers/fix-secrets.d.ts +9 -0
- package/dist/fixers/fix-secrets.js +132 -0
- package/dist/fixers/index.d.ts +11 -0
- package/dist/fixers/index.js +37 -0
- package/dist/fixers/registry.d.ts +25 -0
- package/dist/fixers/registry.js +249 -0
- package/dist/fixers/scheduler.d.ts +9 -0
- package/dist/fixers/scheduler.js +254 -0
- package/dist/fixers/types.d.ts +160 -0
- package/dist/fixers/types.js +11 -0
- package/dist/reporter.d.ts +28 -0
- package/dist/reporter.js +185 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.js +5 -0
- package/package.json +61 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
// @nodatachat/guard — CLI Entry Point
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// npx nodata-guard --license-key NDC-XXXX
|
|
8
|
+
// npx nodata-guard --license-key NDC-XXXX --db postgres://...
|
|
9
|
+
// npx nodata-guard --license-key NDC-XXXX --ci --fail-on critical
|
|
10
|
+
// npx nodata-guard --license-key $NDC_LICENSE --full --db $DATABASE_URL
|
|
11
|
+
//
|
|
12
|
+
// Outputs:
|
|
13
|
+
// ./nodata-full-report.json — Full report (stays local)
|
|
14
|
+
// ./nodata-metadata-only.json — Metadata sent to NoData
|
|
15
|
+
//
|
|
16
|
+
// The customer can diff the two files to verify no data was sent.
|
|
17
|
+
// ═══════════════════════════════════════════════════════════
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
const path_1 = require("path");
|
|
20
|
+
const fs_1 = require("fs");
|
|
21
|
+
const activation_1 = require("./activation");
|
|
22
|
+
const code_scanner_1 = require("./code-scanner");
|
|
23
|
+
const db_scanner_1 = require("./db-scanner");
|
|
24
|
+
const reporter_1 = require("./reporter");
|
|
25
|
+
const registry_1 = require("./fixers/registry");
|
|
26
|
+
const scheduler_1 = require("./fixers/scheduler");
|
|
27
|
+
const VERSION = "2.0.0";
|
|
28
|
+
async function main() {
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
// Parse args
|
|
31
|
+
let licenseKey;
|
|
32
|
+
let dbUrl;
|
|
33
|
+
let projectDir = process.cwd();
|
|
34
|
+
let ciMode = false;
|
|
35
|
+
let failOn = null;
|
|
36
|
+
let outputDir = process.cwd();
|
|
37
|
+
let skipSend = false;
|
|
38
|
+
let fixMode = "none";
|
|
39
|
+
let schedulePreset;
|
|
40
|
+
let ciProvider;
|
|
41
|
+
let onlyFixers;
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
switch (args[i]) {
|
|
44
|
+
case "--license-key":
|
|
45
|
+
licenseKey = args[++i];
|
|
46
|
+
break;
|
|
47
|
+
case "--db":
|
|
48
|
+
dbUrl = args[++i];
|
|
49
|
+
break;
|
|
50
|
+
case "--dir":
|
|
51
|
+
projectDir = (0, path_1.resolve)(args[++i]);
|
|
52
|
+
break;
|
|
53
|
+
case "--output":
|
|
54
|
+
outputDir = (0, path_1.resolve)(args[++i]);
|
|
55
|
+
break;
|
|
56
|
+
case "--ci":
|
|
57
|
+
ciMode = true;
|
|
58
|
+
break;
|
|
59
|
+
case "--fail-on":
|
|
60
|
+
failOn = args[++i];
|
|
61
|
+
break;
|
|
62
|
+
case "--skip-send":
|
|
63
|
+
skipSend = true;
|
|
64
|
+
break;
|
|
65
|
+
case "--fix-plan":
|
|
66
|
+
fixMode = "plan";
|
|
67
|
+
break;
|
|
68
|
+
case "--fix":
|
|
69
|
+
fixMode = "apply";
|
|
70
|
+
break;
|
|
71
|
+
case "--fix-only":
|
|
72
|
+
onlyFixers = args[++i]?.split(",");
|
|
73
|
+
break;
|
|
74
|
+
case "--schedule":
|
|
75
|
+
schedulePreset = args[++i] || "weekly";
|
|
76
|
+
break;
|
|
77
|
+
case "--ci-provider":
|
|
78
|
+
ciProvider = args[++i];
|
|
79
|
+
break;
|
|
80
|
+
case "--help":
|
|
81
|
+
printHelp();
|
|
82
|
+
process.exit(0);
|
|
83
|
+
case "--version":
|
|
84
|
+
console.log(VERSION);
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Env fallbacks
|
|
89
|
+
if (!licenseKey)
|
|
90
|
+
licenseKey = process.env.NDC_LICENSE || process.env.NODATA_LICENSE_KEY;
|
|
91
|
+
if (!dbUrl)
|
|
92
|
+
dbUrl = process.env.DATABASE_URL;
|
|
93
|
+
if (!licenseKey) {
|
|
94
|
+
console.error("\n Error: --license-key required (or set NDC_LICENSE env var)\n");
|
|
95
|
+
printHelp();
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
// ── Schedule setup (no scan needed) ──
|
|
99
|
+
if (schedulePreset) {
|
|
100
|
+
const config = loadOrCreateConfig(projectDir, { dbUrl, failOn });
|
|
101
|
+
config.schedule = { enabled: true, preset: schedulePreset, cron: undefined, timezone: "UTC" };
|
|
102
|
+
const provider = ciProvider || "auto";
|
|
103
|
+
const result = (0, scheduler_1.installSchedule)(projectDir, config, provider);
|
|
104
|
+
console.log("");
|
|
105
|
+
console.log(" ╔══════════════════════════════════════╗");
|
|
106
|
+
console.log(" ║ NoData Guard — Schedule Installed ║");
|
|
107
|
+
console.log(" ╚══════════════════════════════════════╝");
|
|
108
|
+
console.log("");
|
|
109
|
+
result.messages.forEach(m => console.log(` ${m}`));
|
|
110
|
+
console.log("");
|
|
111
|
+
result.files.forEach(f => console.log(` Created: ${f}`));
|
|
112
|
+
console.log("");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const scanType = dbUrl ? "full" : "code";
|
|
116
|
+
const startTime = Date.now();
|
|
117
|
+
// ── Header ──
|
|
118
|
+
if (!ciMode) {
|
|
119
|
+
console.log("");
|
|
120
|
+
console.log(" ╔══════════════════════════════════════╗");
|
|
121
|
+
console.log(" ║ NoData Guard v" + VERSION + " ║");
|
|
122
|
+
console.log(" ║ Security scanner — runs locally ║");
|
|
123
|
+
console.log(" ║ Your data never leaves your machine ║");
|
|
124
|
+
console.log(" ╚══════════════════════════════════════╝");
|
|
125
|
+
console.log("");
|
|
126
|
+
}
|
|
127
|
+
// ── Step 1: Activate ──
|
|
128
|
+
log(ciMode, "Activating license...");
|
|
129
|
+
const activation = await (0, activation_1.activate)({
|
|
130
|
+
licenseKey,
|
|
131
|
+
projectDir,
|
|
132
|
+
scanType,
|
|
133
|
+
});
|
|
134
|
+
if (!activation.success) {
|
|
135
|
+
console.error(` Activation failed: ${activation.error}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
log(ciMode, `Activated. Tier: ${activation.tier} | Scan ID: ${activation.scan_id}`);
|
|
139
|
+
// ── Step 2: Code scan ──
|
|
140
|
+
log(ciMode, "Scanning code...");
|
|
141
|
+
const files = (0, code_scanner_1.readProjectFiles)(projectDir, (count) => {
|
|
142
|
+
if (!ciMode)
|
|
143
|
+
process.stdout.write(`\r Reading files... ${count}`);
|
|
144
|
+
});
|
|
145
|
+
if (!ciMode)
|
|
146
|
+
console.log("");
|
|
147
|
+
log(ciMode, `Scanned ${files.length} files`);
|
|
148
|
+
const piiFields = (0, code_scanner_1.scanPIIFields)(files);
|
|
149
|
+
const routes = (0, code_scanner_1.scanRoutes)(files);
|
|
150
|
+
const secrets = (0, code_scanner_1.scanSecrets)(files);
|
|
151
|
+
const stack = (0, code_scanner_1.detectStack)(files);
|
|
152
|
+
const encryptedCode = piiFields.filter(f => f.encrypted).length;
|
|
153
|
+
log(ciMode, `PII fields: ${piiFields.length} found, ${encryptedCode} encrypted`);
|
|
154
|
+
log(ciMode, `Routes: ${routes.length} total, ${routes.filter(r => r.has_auth).length} protected`);
|
|
155
|
+
log(ciMode, `Secrets: ${secrets.filter(s => !s.is_env_interpolated).length} hardcoded`);
|
|
156
|
+
// ── Step 3: DB scan (if connection string provided) ──
|
|
157
|
+
let dbResult = null;
|
|
158
|
+
if (dbUrl) {
|
|
159
|
+
log(ciMode, "Scanning database...");
|
|
160
|
+
try {
|
|
161
|
+
dbResult = await (0, db_scanner_1.scanDatabase)(dbUrl, (msg) => log(ciMode, ` DB: ${msg}`));
|
|
162
|
+
const encryptedDb = dbResult.pii_fields.filter(f => f.encrypted).length;
|
|
163
|
+
log(ciMode, `DB PII: ${dbResult.pii_fields.length} found, ${encryptedDb} encrypted`);
|
|
164
|
+
log(ciMode, `RLS: ${dbResult.rls.filter(r => r.rls_enabled).length}/${dbResult.rls.length} tables`);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
log(ciMode, `DB scan failed: ${err instanceof Error ? err.message : err}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ── Step 4: Generate dual reports ──
|
|
171
|
+
const scanDuration = Date.now() - startTime;
|
|
172
|
+
const projectHash = (0, activation_1.computeProjectHash)(projectDir);
|
|
173
|
+
const { full, metadata } = (0, reporter_1.generateReports)({
|
|
174
|
+
scanId: activation.scan_id,
|
|
175
|
+
projectName: projectDir.split(/[/\\]/).pop() || "unknown",
|
|
176
|
+
projectHash,
|
|
177
|
+
scanType,
|
|
178
|
+
scanDurationMs: scanDuration,
|
|
179
|
+
previousScore: null, // Will be filled by API response
|
|
180
|
+
code: {
|
|
181
|
+
filesScanned: files.length,
|
|
182
|
+
framework: stack.framework,
|
|
183
|
+
database: stack.database,
|
|
184
|
+
piiFields,
|
|
185
|
+
routes,
|
|
186
|
+
secrets,
|
|
187
|
+
},
|
|
188
|
+
db: dbResult ? {
|
|
189
|
+
tables: dbResult.tables,
|
|
190
|
+
piiFields: dbResult.pii_fields,
|
|
191
|
+
rls: dbResult.rls,
|
|
192
|
+
infra: dbResult.infra,
|
|
193
|
+
} : undefined,
|
|
194
|
+
});
|
|
195
|
+
// ── Step 5: Save reports ──
|
|
196
|
+
const fullPath = (0, path_1.resolve)(outputDir, "nodata-full-report.json");
|
|
197
|
+
const metaPath = (0, path_1.resolve)(outputDir, "nodata-metadata-only.json");
|
|
198
|
+
(0, fs_1.writeFileSync)(fullPath, JSON.stringify(full, null, 2), "utf-8");
|
|
199
|
+
(0, fs_1.writeFileSync)(metaPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
200
|
+
log(ciMode, `Full report: ${fullPath}`);
|
|
201
|
+
log(ciMode, `Metadata only: ${metaPath}`);
|
|
202
|
+
// ── Step 6: Send metadata to NoData ──
|
|
203
|
+
let serverResponse = {};
|
|
204
|
+
if (!skipSend) {
|
|
205
|
+
log(ciMode, "Sending metadata to NoData...");
|
|
206
|
+
try {
|
|
207
|
+
const res = await fetch("https://nodatachat.com/api/guard/report", {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: {
|
|
210
|
+
"Content-Type": "application/json",
|
|
211
|
+
"X-Scan-Id": activation.scan_id,
|
|
212
|
+
"X-Activation-Key": activation.activation_key,
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify(metadata),
|
|
215
|
+
signal: AbortSignal.timeout(10000),
|
|
216
|
+
});
|
|
217
|
+
if (res.ok) {
|
|
218
|
+
serverResponse = await res.json();
|
|
219
|
+
log(ciMode, `Sent. ${serverResponse.improved ? "Score improved!" : "Score tracked."}`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
log(ciMode, `Send failed (${res.status}) — report saved locally`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
log(ciMode, "Could not reach NoData API — report saved locally");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ── Step 7: Print summary ──
|
|
230
|
+
if (!ciMode) {
|
|
231
|
+
console.log("");
|
|
232
|
+
console.log(" ══════════════════════════════════════");
|
|
233
|
+
console.log(" GUARD RESULTS");
|
|
234
|
+
console.log(" ══════════════════════════════════════");
|
|
235
|
+
console.log(` Score: ${full.overall_score}%${serverResponse.previous_score != null ? ` (was ${serverResponse.previous_score}%)` : ""}`);
|
|
236
|
+
console.log(` PII fields: ${full.summary.encrypted_fields}/${full.summary.total_pii_fields} encrypted (${full.summary.coverage_percent}%)`);
|
|
237
|
+
if (full.code) {
|
|
238
|
+
console.log(` Routes: ${full.code.routes.filter(r => r.has_auth).length}/${full.code.routes.length} protected`);
|
|
239
|
+
console.log(` Secrets: ${full.summary.critical_issues} critical, ${full.summary.high_issues} high`);
|
|
240
|
+
}
|
|
241
|
+
if (full.db) {
|
|
242
|
+
console.log(` DB encryption: ${full.db.encryption_coverage_percent}%`);
|
|
243
|
+
console.log(` RLS coverage: ${full.db.rls_coverage_percent}%`);
|
|
244
|
+
}
|
|
245
|
+
console.log(" ──────────────────────────────────────");
|
|
246
|
+
console.log(` Full report: ${fullPath}`);
|
|
247
|
+
console.log(` Sent to NoData: ${metaPath}`);
|
|
248
|
+
console.log(` Proof hash: ${full.proof_hash.slice(0, 16)}...`);
|
|
249
|
+
console.log(" ══════════════════════════════════════");
|
|
250
|
+
console.log(" Your data never left your machine.");
|
|
251
|
+
console.log(" Diff the two files to verify.\n");
|
|
252
|
+
}
|
|
253
|
+
// ── Step 8: Capsule — Fix Plan / Apply ──
|
|
254
|
+
if (fixMode !== "none") {
|
|
255
|
+
const config = loadOrCreateConfig(projectDir, { dbUrl, failOn });
|
|
256
|
+
const fixerContext = {
|
|
257
|
+
projectDir,
|
|
258
|
+
dbUrl,
|
|
259
|
+
config,
|
|
260
|
+
scanResults: {
|
|
261
|
+
piiFields: piiFields.map(f => ({
|
|
262
|
+
table: f.table, column: f.column, pii_type: f.pii_type,
|
|
263
|
+
encrypted: f.encrypted, encryption_pattern: f.encryption_pattern,
|
|
264
|
+
})),
|
|
265
|
+
routes: routes.map(r => ({ path: r.path, has_auth: r.has_auth, auth_type: r.auth_type })),
|
|
266
|
+
secrets: secrets.map(s => ({
|
|
267
|
+
file: s.file, line: s.line, type: s.type,
|
|
268
|
+
severity: s.severity, is_env_interpolated: s.is_env_interpolated,
|
|
269
|
+
})),
|
|
270
|
+
rls: dbResult?.rls || [],
|
|
271
|
+
framework: stack.framework,
|
|
272
|
+
database: stack.database,
|
|
273
|
+
},
|
|
274
|
+
stack: {
|
|
275
|
+
framework: stack.framework,
|
|
276
|
+
database: stack.database,
|
|
277
|
+
language: "typescript",
|
|
278
|
+
hosting: "vercel",
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
const capsuleResult = await (0, registry_1.runCapsule)(fixerContext, {
|
|
282
|
+
mode: fixMode === "plan" ? "plan" : "apply",
|
|
283
|
+
fixers: onlyFixers,
|
|
284
|
+
dryRun: fixMode === "plan",
|
|
285
|
+
}, (msg) => log(ciMode, msg));
|
|
286
|
+
if (!ciMode) {
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log(" ══════════════════════════════════════");
|
|
289
|
+
console.log(` CAPSULE ${fixMode === "plan" ? "PLAN" : "RESULTS"}`);
|
|
290
|
+
console.log(" ══════════════════════════════════════");
|
|
291
|
+
console.log(` Total actions: ${capsuleResult.totalActions}`);
|
|
292
|
+
if (fixMode === "apply") {
|
|
293
|
+
console.log(` Applied: ${capsuleResult.applied}`);
|
|
294
|
+
console.log(` Failed: ${capsuleResult.failed}`);
|
|
295
|
+
console.log(` Skipped: ${capsuleResult.skipped}`);
|
|
296
|
+
}
|
|
297
|
+
console.log(` Manual pending: ${capsuleResult.manualPending}`);
|
|
298
|
+
console.log(` Est. impact: +${capsuleResult.estimatedScoreImpact}% score`);
|
|
299
|
+
console.log(` Proof hash: ${capsuleResult.proofHash.slice(0, 16)}...`);
|
|
300
|
+
console.log(" ──────────────────────────────────────");
|
|
301
|
+
// Print per-fixer summary
|
|
302
|
+
for (const plan of capsuleResult.plans) {
|
|
303
|
+
const icon = plan.actions.every(a => a.status === "applied") ? "✅"
|
|
304
|
+
: plan.actions.some(a => a.status === "failed") ? "❌" : "📋";
|
|
305
|
+
console.log(` ${icon} ${plan.nameHe}: ${plan.totalActions} actions (${plan.autoFixable} auto, ${plan.manualRequired} manual)`);
|
|
306
|
+
}
|
|
307
|
+
console.log(" ══════════════════════════════════════\n");
|
|
308
|
+
// Write fix plans to output dir
|
|
309
|
+
if (capsuleResult.plans.length > 0) {
|
|
310
|
+
const plansPath = (0, path_1.resolve)(outputDir, "nodata-fix-plan.json");
|
|
311
|
+
(0, fs_1.writeFileSync)(plansPath, JSON.stringify(capsuleResult, null, 2), "utf-8");
|
|
312
|
+
log(ciMode, `Fix plan saved: ${plansPath}`);
|
|
313
|
+
// Write SQL migrations to files
|
|
314
|
+
for (const plan of capsuleResult.plans) {
|
|
315
|
+
for (const action of plan.actions) {
|
|
316
|
+
if (action.type === "file-create" && action.target.includes("migrations/")) {
|
|
317
|
+
const migPath = (0, path_1.resolve)(outputDir, action.target);
|
|
318
|
+
const migDir = (0, path_1.resolve)(outputDir, "migrations");
|
|
319
|
+
if (!(0, fs_1.existsSync)(migDir)) {
|
|
320
|
+
const { mkdirSync } = require("fs");
|
|
321
|
+
mkdirSync(migDir, { recursive: true });
|
|
322
|
+
}
|
|
323
|
+
(0, fs_1.writeFileSync)(migPath, action.content, "utf-8");
|
|
324
|
+
log(ciMode, `Migration: ${migPath}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Send notifications if configured
|
|
331
|
+
if (config.notify && fixMode === "apply") {
|
|
332
|
+
const notifyPayload = {
|
|
333
|
+
event: capsuleResult.applied > 0 ? "fix-applied" : "scan-complete",
|
|
334
|
+
timestamp: new Date().toISOString(),
|
|
335
|
+
projectName: full.project_name || "unknown",
|
|
336
|
+
projectHash: full.proof_hash?.slice(0, 16) || "",
|
|
337
|
+
score: full.overall_score,
|
|
338
|
+
previousScore: serverResponse.previous_score ?? null,
|
|
339
|
+
delta: serverResponse.previous_score != null ? full.overall_score - serverResponse.previous_score : 0,
|
|
340
|
+
critical: full.summary.critical_issues,
|
|
341
|
+
high: full.summary.high_issues,
|
|
342
|
+
medium: full.summary.medium_issues,
|
|
343
|
+
fixesApplied: capsuleResult.applied,
|
|
344
|
+
fixesFailed: capsuleResult.failed,
|
|
345
|
+
scanId: activation.scan_id,
|
|
346
|
+
proofHash: capsuleResult.proofHash,
|
|
347
|
+
};
|
|
348
|
+
await (0, registry_1.sendNotifications)(config, notifyPayload.event, notifyPayload, (msg) => log(ciMode, msg));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ── CI mode: exit code ──
|
|
352
|
+
if (ciMode && failOn) {
|
|
353
|
+
const { critical_issues, high_issues, medium_issues } = full.summary;
|
|
354
|
+
let shouldFail = false;
|
|
355
|
+
if (failOn === "critical" && critical_issues > 0)
|
|
356
|
+
shouldFail = true;
|
|
357
|
+
if (failOn === "high" && (critical_issues + high_issues) > 0)
|
|
358
|
+
shouldFail = true;
|
|
359
|
+
if (failOn === "medium" && (critical_issues + high_issues + medium_issues) > 0)
|
|
360
|
+
shouldFail = true;
|
|
361
|
+
if (shouldFail) {
|
|
362
|
+
console.log(`::error::NoData Guard: ${critical_issues} critical, ${high_issues} high, ${medium_issues} medium issues`);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
console.log(`NoData Guard: PASS (score ${full.overall_score}%)`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function log(ci, msg) {
|
|
371
|
+
if (ci) {
|
|
372
|
+
console.log(`[nodata-guard] ${msg}`);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
console.log(` ${msg}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function loadOrCreateConfig(projectDir, overrides) {
|
|
379
|
+
const configPath = (0, path_1.resolve)(projectDir, ".nodata-guard.json");
|
|
380
|
+
if ((0, fs_1.existsSync)(configPath)) {
|
|
381
|
+
try {
|
|
382
|
+
return JSON.parse((0, fs_1.readFileSync)(configPath, "utf-8"));
|
|
383
|
+
}
|
|
384
|
+
catch { /* fall through */ }
|
|
385
|
+
}
|
|
386
|
+
return (0, scheduler_1.generateDefaultConfig)({
|
|
387
|
+
scan: {
|
|
388
|
+
dbUrl: overrides.dbUrl,
|
|
389
|
+
failOn: overrides.failOn || "critical",
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function printHelp() {
|
|
394
|
+
console.log(`
|
|
395
|
+
NoData Guard v${VERSION} — Security Scanner + Auto-Remediation Capsule
|
|
396
|
+
|
|
397
|
+
Usage:
|
|
398
|
+
npx nodata-guard --license-key NDC-XXXX # Scan only
|
|
399
|
+
npx nodata-guard --license-key NDC-XXXX --fix-plan # Scan + show fix plan
|
|
400
|
+
npx nodata-guard --license-key NDC-XXXX --fix # Scan + apply fixes
|
|
401
|
+
npx nodata-guard --license-key NDC-XXXX --schedule weekly # Setup CI schedule
|
|
402
|
+
npx nodata-guard --license-key NDC-XXXX --db $DATABASE_URL --fix # Full scan + fix
|
|
403
|
+
|
|
404
|
+
Scan Options:
|
|
405
|
+
--license-key <key> NoData license key (or set NDC_LICENSE env var)
|
|
406
|
+
--db <url> Database connection string for DB probe
|
|
407
|
+
--dir <path> Project directory (default: cwd)
|
|
408
|
+
--output <path> Output directory for reports (default: cwd)
|
|
409
|
+
--ci CI mode — minimal output, exit codes
|
|
410
|
+
--fail-on <level> Exit 1 if issues at: critical | high | medium
|
|
411
|
+
--skip-send Don't send metadata to NoData
|
|
412
|
+
|
|
413
|
+
Capsule Options:
|
|
414
|
+
--fix-plan Generate fix plan (dry-run, no changes)
|
|
415
|
+
--fix Apply all auto-fixable remediation
|
|
416
|
+
--fix-only <fixers> Only run specific fixers (comma-separated)
|
|
417
|
+
Fixers: pii-encrypt, rls, secrets, routes-auth,
|
|
418
|
+
headers, csrf, rate-limit, gitignore
|
|
419
|
+
|
|
420
|
+
Schedule Options:
|
|
421
|
+
--schedule <preset> Install CI workflow: daily | weekly | monthly
|
|
422
|
+
--ci-provider <name> CI provider: github-actions | gitlab-ci | bitbucket | auto
|
|
423
|
+
|
|
424
|
+
Notifications:
|
|
425
|
+
Configure in .nodata-guard.json → notify: { email, webhook, slack, telegram }
|
|
426
|
+
|
|
427
|
+
Output files:
|
|
428
|
+
nodata-full-report.json Full report — STAYS LOCAL
|
|
429
|
+
nodata-metadata-only.json Metadata only — sent to NoData
|
|
430
|
+
nodata-fix-plan.json Fix plan with actions
|
|
431
|
+
migrations/*.sql Generated SQL migrations
|
|
432
|
+
|
|
433
|
+
What we NEVER receive:
|
|
434
|
+
Data values, emails, phones, passwords, source code, connection strings.
|
|
435
|
+
|
|
436
|
+
Examples:
|
|
437
|
+
# Quick scan
|
|
438
|
+
npx nodata-guard --license-key NDC-XXXX
|
|
439
|
+
|
|
440
|
+
# Full scan + DB probe + fix
|
|
441
|
+
npx nodata-guard --license-key NDC-XXXX --db $DATABASE_URL --fix
|
|
442
|
+
|
|
443
|
+
# Only fix security headers and CSRF
|
|
444
|
+
npx nodata-guard --license-key NDC-XXXX --fix --fix-only headers,csrf
|
|
445
|
+
|
|
446
|
+
# Setup weekly CI scan with GitHub Actions
|
|
447
|
+
npx nodata-guard --license-key NDC-XXXX --schedule weekly
|
|
448
|
+
|
|
449
|
+
# CI pipeline
|
|
450
|
+
npx nodata-guard --ci --fail-on critical
|
|
451
|
+
|
|
452
|
+
Documentation: https://nodatachat.com/guard
|
|
453
|
+
`);
|
|
454
|
+
}
|
|
455
|
+
main().catch((err) => {
|
|
456
|
+
console.error("Fatal:", err);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PIIFieldResult, RouteResult, SecretResult } from "./types";
|
|
2
|
+
interface FileEntry {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function readProjectFiles(projectDir: string, onProgress?: (count: number) => void): FileEntry[];
|
|
7
|
+
export declare function scanPIIFields(files: FileEntry[]): PIIFieldResult[];
|
|
8
|
+
export declare function scanRoutes(files: FileEntry[]): RouteResult[];
|
|
9
|
+
export declare function scanSecrets(files: FileEntry[]): SecretResult[];
|
|
10
|
+
export declare function detectStack(files: FileEntry[]): {
|
|
11
|
+
framework: string;
|
|
12
|
+
database: string;
|
|
13
|
+
};
|
|
14
|
+
export {};
|