@nodatachat/guard 2.3.0 → 2.4.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/capsule-dir.d.ts +11 -0
- package/dist/capsule-dir.js +218 -1
- package/dist/cli.js +138 -4
- package/dist/code-scanner.d.ts +1 -1
- package/dist/code-scanner.js +25 -2
- package/dist/reporter.js +7 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/capsule-dir.d.ts
CHANGED
|
@@ -91,3 +91,14 @@ export declare function getScoreHistory(projectDir: string): ScoreSnapshot[];
|
|
|
91
91
|
* Print .capsule/ status to terminal.
|
|
92
92
|
*/
|
|
93
93
|
export declare function printCapsuleStatus(projectDir: string, ciMode: boolean): void;
|
|
94
|
+
export declare function attestFinding(projectDir: string, licenseKey: string, findingId: string, status: Override["status"], note: string): void;
|
|
95
|
+
/**
|
|
96
|
+
* Subcommand: guard status
|
|
97
|
+
* Print full .capsule/ status without running a scan.
|
|
98
|
+
*/
|
|
99
|
+
export declare function printFullStatus(projectDir: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* Subcommand: guard diff
|
|
102
|
+
* Show what changed between the two most recent scans.
|
|
103
|
+
*/
|
|
104
|
+
export declare function printDiff(projectDir: string): void;
|
package/dist/capsule-dir.js
CHANGED
|
@@ -30,6 +30,9 @@ exports.readProof = readProof;
|
|
|
30
30
|
exports.readOverrides = readOverrides;
|
|
31
31
|
exports.getScoreHistory = getScoreHistory;
|
|
32
32
|
exports.printCapsuleStatus = printCapsuleStatus;
|
|
33
|
+
exports.attestFinding = attestFinding;
|
|
34
|
+
exports.printFullStatus = printFullStatus;
|
|
35
|
+
exports.printDiff = printDiff;
|
|
33
36
|
const path_1 = require("path");
|
|
34
37
|
const fs_1 = require("fs");
|
|
35
38
|
const crypto_1 = require("crypto");
|
|
@@ -200,7 +203,7 @@ function getScoreHistory(projectDir) {
|
|
|
200
203
|
const scoresDir = (0, path_1.join)(projectDir, DIR_NAME, "scores");
|
|
201
204
|
if (!(0, fs_1.existsSync)(scoresDir))
|
|
202
205
|
return [];
|
|
203
|
-
const files =
|
|
206
|
+
const files = (0, fs_1.readdirSync)(scoresDir)
|
|
204
207
|
.filter((f) => f.endsWith(".json"))
|
|
205
208
|
.sort();
|
|
206
209
|
return files.map((f) => {
|
|
@@ -246,3 +249,217 @@ function printCapsuleStatus(projectDir, ciMode) {
|
|
|
246
249
|
console.log(" \x1b[2mAll evidence stays local. Commit to git for audit trail.\x1b[0m");
|
|
247
250
|
console.log(" ══════════════════════════════════════\n");
|
|
248
251
|
}
|
|
252
|
+
// ═══════════════════════════════════════════════════════════
|
|
253
|
+
// Subcommand: guard attest
|
|
254
|
+
//
|
|
255
|
+
// Allows the user to manually attest that a finding has been
|
|
256
|
+
// addressed. Creates a signed entry in overrides.json.
|
|
257
|
+
// ═══════════════════════════════════════════════════════════
|
|
258
|
+
function attestFinding(projectDir, licenseKey, findingId, status, note) {
|
|
259
|
+
const capsuleDir = (0, path_1.join)(projectDir, DIR_NAME);
|
|
260
|
+
if (!(0, fs_1.existsSync)(capsuleDir)) {
|
|
261
|
+
initCapsuleDir(projectDir, licenseKey);
|
|
262
|
+
}
|
|
263
|
+
const overridesPath = (0, path_1.join)(capsuleDir, "overrides.json");
|
|
264
|
+
const overrides = (0, fs_1.existsSync)(overridesPath)
|
|
265
|
+
? JSON.parse((0, fs_1.readFileSync)(overridesPath, "utf-8"))
|
|
266
|
+
: { version: "1.0", overrides: [] };
|
|
267
|
+
const deviceHash = (0, crypto_1.createHash)("sha256")
|
|
268
|
+
.update(`capsule:device:${require("os").hostname()}`)
|
|
269
|
+
.digest("hex").slice(0, 12);
|
|
270
|
+
const now = new Date().toISOString();
|
|
271
|
+
const payload = JSON.stringify({ finding_id: findingId, status, note, attested_by: deviceHash, attested_at: now });
|
|
272
|
+
const hmac = signEvidence(payload, licenseKey);
|
|
273
|
+
// Update existing or add new
|
|
274
|
+
const existing = overrides.overrides.findIndex(o => o.finding_id === findingId);
|
|
275
|
+
const entry = { finding_id: findingId, status, note, attested_by: deviceHash, attested_at: now, hmac };
|
|
276
|
+
if (existing >= 0) {
|
|
277
|
+
overrides.overrides[existing] = entry;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
overrides.overrides.push(entry);
|
|
281
|
+
}
|
|
282
|
+
(0, fs_1.writeFileSync)(overridesPath, JSON.stringify(overrides, null, 2), "utf-8");
|
|
283
|
+
// Also log as evidence
|
|
284
|
+
addEvidence(capsuleDir, licenseKey, {
|
|
285
|
+
date: now,
|
|
286
|
+
action: "override_added",
|
|
287
|
+
category: status === "fixed" ? "remediation" : "risk_management",
|
|
288
|
+
detail: `Attested "${findingId}" as ${status}: ${note}`,
|
|
289
|
+
impact: `Override: ${status}`,
|
|
290
|
+
attested_by: deviceHash,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Subcommand: guard status
|
|
295
|
+
* Print full .capsule/ status without running a scan.
|
|
296
|
+
*/
|
|
297
|
+
function printFullStatus(projectDir) {
|
|
298
|
+
const capsuleDir = (0, path_1.join)(projectDir, DIR_NAME);
|
|
299
|
+
if (!(0, fs_1.existsSync)(capsuleDir)) {
|
|
300
|
+
console.log("\n No .capsule/ directory found. Run a scan first.\n");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const history = getScoreHistory(projectDir);
|
|
304
|
+
const proof = readProof(projectDir);
|
|
305
|
+
const overrides = readOverrides(projectDir);
|
|
306
|
+
console.log("");
|
|
307
|
+
console.log(" ╔══════════════════════════════════════╗");
|
|
308
|
+
console.log(" ║ NoData Guard — Status ║");
|
|
309
|
+
console.log(" ╚══════════════════════════════════════╝");
|
|
310
|
+
console.log("");
|
|
311
|
+
// ── Score history ──
|
|
312
|
+
console.log(" \x1b[33m── SCORE HISTORY ──\x1b[0m");
|
|
313
|
+
if (history.length === 0) {
|
|
314
|
+
console.log(" No scans recorded yet.\n");
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
const last = history[history.length - 1];
|
|
318
|
+
console.log(` Latest score: \x1b[1m${last.score}%\x1b[0m (${last.scan_type} scan, Guard v${last.guard_version})`);
|
|
319
|
+
console.log(` Last scan date: ${last.date}`);
|
|
320
|
+
console.log(` Total scans: ${history.length}`);
|
|
321
|
+
if (last.code_score != null)
|
|
322
|
+
console.log(` Code score: ${last.code_score}%`);
|
|
323
|
+
if (last.db_score != null)
|
|
324
|
+
console.log(` DB score: ${last.db_score}%`);
|
|
325
|
+
console.log(` PII coverage: ${last.pii_encrypted}/${last.pii_total} encrypted (${last.coverage_percent}%)`);
|
|
326
|
+
console.log(` Issues: ${last.critical} critical, ${last.high} high, ${last.medium} medium, ${last.low} low`);
|
|
327
|
+
console.log(` Findings: ${last.findings_count} total`);
|
|
328
|
+
if (history.length >= 2) {
|
|
329
|
+
console.log("");
|
|
330
|
+
console.log(" \x1b[33m── TREND ──\x1b[0m");
|
|
331
|
+
const maxShow = Math.min(history.length, 10);
|
|
332
|
+
for (let i = history.length - maxShow; i < history.length; i++) {
|
|
333
|
+
const h = history[i];
|
|
334
|
+
const prev = i > 0 ? history[i - 1] : null;
|
|
335
|
+
const delta = prev ? h.score - prev.score : 0;
|
|
336
|
+
const arrow = delta > 0 ? `\x1b[32m+${delta}\x1b[0m` : delta < 0 ? `\x1b[31m${delta}\x1b[0m` : " 0";
|
|
337
|
+
const bar = "\x1b[32m" + "█".repeat(Math.round(h.score / 5)) + "\x1b[0m" + "░".repeat(20 - Math.round(h.score / 5));
|
|
338
|
+
console.log(` ${h.date} ${bar} ${h.score}% (${arrow})`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
console.log("");
|
|
342
|
+
}
|
|
343
|
+
// ── Proof ──
|
|
344
|
+
console.log(" \x1b[33m── DB-VERIFIED ENCRYPTION ──\x1b[0m");
|
|
345
|
+
if (!proof || proof.fields.length === 0) {
|
|
346
|
+
console.log(" No DB-verified encryption yet. Run with --db to verify.\n");
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
console.log(` ${proof.fields.length} field(s) verified in database:\n`);
|
|
350
|
+
for (const f of proof.fields) {
|
|
351
|
+
const cov = f.coverage_percent;
|
|
352
|
+
const covColor = cov >= 90 ? "\x1b[32m" : cov >= 50 ? "\x1b[33m" : "\x1b[31m";
|
|
353
|
+
console.log(` ${f.table_hash}:${f.column_hash} ${f.pattern} ${covColor}${cov}%\x1b[0m (${f.encrypted_count}/${f.row_count} rows)`);
|
|
354
|
+
}
|
|
355
|
+
console.log(`\n Last verified: ${proof.updated_at}\n`);
|
|
356
|
+
}
|
|
357
|
+
// ── Overrides ──
|
|
358
|
+
console.log(" \x1b[33m── MANUAL ATTESTATIONS ──\x1b[0m");
|
|
359
|
+
if (!overrides || overrides.overrides.length === 0) {
|
|
360
|
+
console.log(" No overrides. Use \x1b[36mguard attest\x1b[0m to declare fixes.\n");
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
for (const o of overrides.overrides) {
|
|
364
|
+
const statusColor = o.status === "fixed" ? "\x1b[32m" : o.status === "accepted_risk" ? "\x1b[33m" : "\x1b[36m";
|
|
365
|
+
console.log(` ${o.finding_id}`);
|
|
366
|
+
console.log(` Status: ${statusColor}${o.status}\x1b[0m`);
|
|
367
|
+
console.log(` Note: ${o.note}`);
|
|
368
|
+
console.log(` Date: ${o.attested_at}`);
|
|
369
|
+
console.log(` Device: ${o.attested_by}`);
|
|
370
|
+
console.log("");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// ── Evidence summary ──
|
|
374
|
+
const evidenceDir = (0, path_1.join)(capsuleDir, "evidence");
|
|
375
|
+
if ((0, fs_1.existsSync)(evidenceDir)) {
|
|
376
|
+
const evidenceFiles = (0, fs_1.readdirSync)(evidenceDir).filter(f => f.endsWith(".json"));
|
|
377
|
+
let totalEntries = 0;
|
|
378
|
+
for (const ef of evidenceFiles) {
|
|
379
|
+
try {
|
|
380
|
+
const entries = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(evidenceDir, ef), "utf-8"));
|
|
381
|
+
totalEntries += entries.length;
|
|
382
|
+
}
|
|
383
|
+
catch { /* skip */ }
|
|
384
|
+
}
|
|
385
|
+
console.log(" \x1b[33m── EVIDENCE LOG ──\x1b[0m");
|
|
386
|
+
console.log(` ${totalEntries} entries across ${evidenceFiles.length} day(s)`);
|
|
387
|
+
console.log(" \x1b[2mAll evidence stays local. Commit .capsule/ to git for audit trail.\x1b[0m\n");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Subcommand: guard diff
|
|
392
|
+
* Show what changed between the two most recent scans.
|
|
393
|
+
*/
|
|
394
|
+
function printDiff(projectDir) {
|
|
395
|
+
const history = getScoreHistory(projectDir);
|
|
396
|
+
if (history.length < 2) {
|
|
397
|
+
console.log("\n Need at least 2 scans to show a diff. Run another scan first.\n");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const prev = history[history.length - 2];
|
|
401
|
+
const curr = history[history.length - 1];
|
|
402
|
+
console.log("");
|
|
403
|
+
console.log(" ╔══════════════════════════════════════╗");
|
|
404
|
+
console.log(" ║ NoData Guard — Diff ║");
|
|
405
|
+
console.log(" ╚══════════════════════════════════════╝");
|
|
406
|
+
console.log("");
|
|
407
|
+
console.log(` Comparing: ${prev.date} → ${curr.date}`);
|
|
408
|
+
console.log("");
|
|
409
|
+
// Score delta
|
|
410
|
+
const scoreDelta = curr.score - prev.score;
|
|
411
|
+
const scoreArrow = scoreDelta > 0 ? `\x1b[32m↑ +${scoreDelta}%\x1b[0m` : scoreDelta < 0 ? `\x1b[31m↓ ${scoreDelta}%\x1b[0m` : "→ no change";
|
|
412
|
+
console.log(` Overall score: ${prev.score}% → ${curr.score}% ${scoreArrow}`);
|
|
413
|
+
// Code score delta
|
|
414
|
+
if (prev.code_score != null && curr.code_score != null) {
|
|
415
|
+
const cd = curr.code_score - prev.code_score;
|
|
416
|
+
const ca = cd > 0 ? `\x1b[32m+${cd}\x1b[0m` : cd < 0 ? `\x1b[31m${cd}\x1b[0m` : "0";
|
|
417
|
+
console.log(` Code score: ${prev.code_score}% → ${curr.code_score}% (${ca})`);
|
|
418
|
+
}
|
|
419
|
+
// DB score delta
|
|
420
|
+
if (prev.db_score != null && curr.db_score != null) {
|
|
421
|
+
const dd = curr.db_score - prev.db_score;
|
|
422
|
+
const da = dd > 0 ? `\x1b[32m+${dd}\x1b[0m` : dd < 0 ? `\x1b[31m${dd}\x1b[0m` : "0";
|
|
423
|
+
console.log(` DB score: ${prev.db_score}% → ${curr.db_score}% (${da})`);
|
|
424
|
+
}
|
|
425
|
+
console.log("");
|
|
426
|
+
// PII coverage
|
|
427
|
+
const piiDelta = curr.coverage_percent - prev.coverage_percent;
|
|
428
|
+
const piiArrow = piiDelta > 0 ? `\x1b[32m+${piiDelta}%\x1b[0m` : piiDelta < 0 ? `\x1b[31m${piiDelta}%\x1b[0m` : "0";
|
|
429
|
+
console.log(` PII encrypted: ${prev.pii_encrypted}/${prev.pii_total} → ${curr.pii_encrypted}/${curr.pii_total} (${piiArrow})`);
|
|
430
|
+
// Findings delta
|
|
431
|
+
const findDelta = curr.findings_count - prev.findings_count;
|
|
432
|
+
const findArrow = findDelta < 0 ? `\x1b[32m${findDelta} resolved\x1b[0m` : findDelta > 0 ? `\x1b[31m+${findDelta} new\x1b[0m` : "no change";
|
|
433
|
+
console.log(` Findings: ${prev.findings_count} → ${curr.findings_count} (${findArrow})`);
|
|
434
|
+
// Issues breakdown
|
|
435
|
+
console.log("");
|
|
436
|
+
console.log(" \x1b[33m── ISSUES BREAKDOWN ──\x1b[0m");
|
|
437
|
+
const showDelta = (label, p, c) => {
|
|
438
|
+
const d = c - p;
|
|
439
|
+
const color = d < 0 ? "\x1b[32m" : d > 0 ? "\x1b[31m" : "";
|
|
440
|
+
const sign = d > 0 ? "+" : "";
|
|
441
|
+
console.log(` ${label.padEnd(12)} ${p} → ${c} ${color}${d !== 0 ? `(${sign}${d})` : "(=)"}\x1b[0m`);
|
|
442
|
+
};
|
|
443
|
+
showDelta("Critical:", prev.critical, curr.critical);
|
|
444
|
+
showDelta("High:", prev.high, curr.high);
|
|
445
|
+
showDelta("Medium:", prev.medium, curr.medium);
|
|
446
|
+
showDelta("Low:", prev.low, curr.low);
|
|
447
|
+
// Scan type change
|
|
448
|
+
if (prev.scan_type !== curr.scan_type) {
|
|
449
|
+
console.log(`\n \x1b[36mNote:\x1b[0m Scan type changed from "${prev.scan_type}" to "${curr.scan_type}"`);
|
|
450
|
+
}
|
|
451
|
+
// Guard version change
|
|
452
|
+
if (prev.guard_version !== curr.guard_version) {
|
|
453
|
+
console.log(` \x1b[36mNote:\x1b[0m Guard version changed from v${prev.guard_version} to v${curr.guard_version}`);
|
|
454
|
+
}
|
|
455
|
+
console.log("");
|
|
456
|
+
if (scoreDelta > 0) {
|
|
457
|
+
console.log(" \x1b[32m✓ Score improved! Keep going.\x1b[0m\n");
|
|
458
|
+
}
|
|
459
|
+
else if (scoreDelta < 0) {
|
|
460
|
+
console.log(" \x1b[31m⚠ Score dropped. Check the full report for new issues.\x1b[0m\n");
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
console.log(" \x1b[33m→ No score change. Fix open findings to improve.\x1b[0m\n");
|
|
464
|
+
}
|
|
465
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -26,9 +26,24 @@ 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.4.0";
|
|
30
30
|
async function main() {
|
|
31
31
|
const args = process.argv.slice(2);
|
|
32
|
+
// ── Subcommand routing ──
|
|
33
|
+
const subcommand = args[0];
|
|
34
|
+
if (subcommand === "status") {
|
|
35
|
+
const dir = args.includes("--dir") ? (0, path_1.resolve)(args[args.indexOf("--dir") + 1]) : process.cwd();
|
|
36
|
+
(0, capsule_dir_1.printFullStatus)(dir);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (subcommand === "diff") {
|
|
40
|
+
const dir = args.includes("--dir") ? (0, path_1.resolve)(args[args.indexOf("--dir") + 1]) : process.cwd();
|
|
41
|
+
(0, capsule_dir_1.printDiff)(dir);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (subcommand === "attest") {
|
|
45
|
+
return handleAttest(args.slice(1));
|
|
46
|
+
}
|
|
32
47
|
// Parse args
|
|
33
48
|
let licenseKey;
|
|
34
49
|
let dbUrl;
|
|
@@ -189,7 +204,7 @@ async function main() {
|
|
|
189
204
|
if (!ciMode)
|
|
190
205
|
console.log("");
|
|
191
206
|
log(ciMode, `Scanned ${files.length} files`);
|
|
192
|
-
const piiFields = (0, code_scanner_1.scanPIIFields)(files);
|
|
207
|
+
const piiFields = (0, code_scanner_1.scanPIIFields)(files, projectDir);
|
|
193
208
|
const routes = (0, code_scanner_1.scanRoutes)(files);
|
|
194
209
|
const secrets = (0, code_scanner_1.scanSecrets)(files);
|
|
195
210
|
const stack = (0, code_scanner_1.detectStack)(files);
|
|
@@ -361,13 +376,30 @@ async function main() {
|
|
|
361
376
|
log(ciMode, `Vault upload error: ${err instanceof Error ? err.message : err}`);
|
|
362
377
|
}
|
|
363
378
|
}
|
|
364
|
-
// ── Step 7: Print summary ──
|
|
379
|
+
// ── Step 7: Print summary with two-score display ──
|
|
380
|
+
const codeScore = full.code ? Math.round((full.code.encryption_coverage_percent * 0.5) +
|
|
381
|
+
(full.code.route_protection_percent * 0.25) +
|
|
382
|
+
(Math.max(0, 100 - (full.summary.critical_issues * 25 + full.summary.high_issues * 10)) * 0.25)) : null;
|
|
383
|
+
const dbScore = full.db ? Math.round((full.db.encryption_coverage_percent * 0.6) +
|
|
384
|
+
(full.db.rls_coverage_percent * 0.4)) : null;
|
|
385
|
+
// Attach to full report for .capsule/ storage
|
|
386
|
+
full.code_score = codeScore;
|
|
387
|
+
full.db_score = dbScore;
|
|
365
388
|
if (!ciMode) {
|
|
366
389
|
console.log("");
|
|
367
390
|
console.log(" ══════════════════════════════════════");
|
|
368
391
|
console.log(" GUARD RESULTS");
|
|
369
392
|
console.log(" ══════════════════════════════════════");
|
|
370
|
-
console.log(`
|
|
393
|
+
console.log(` Overall score: \x1b[1m${full.overall_score}%\x1b[0m${serverResponse.previous_score != null ? ` (was ${serverResponse.previous_score}%)` : ""}`);
|
|
394
|
+
if (codeScore != null) {
|
|
395
|
+
console.log(` Code score: ${codeScore}%`);
|
|
396
|
+
}
|
|
397
|
+
if (dbScore != null) {
|
|
398
|
+
console.log(` DB score: ${dbScore}%`);
|
|
399
|
+
}
|
|
400
|
+
if (codeScore != null && dbScore != null) {
|
|
401
|
+
console.log(" ──────────────────────────────────────");
|
|
402
|
+
}
|
|
371
403
|
console.log(` PII fields: ${full.summary.encrypted_fields}/${full.summary.total_pii_fields} encrypted (${full.summary.coverage_percent}%)`);
|
|
372
404
|
if (full.code) {
|
|
373
405
|
console.log(` Routes: ${full.code.routes.filter(r => r.has_auth).length}/${full.code.routes.length} protected`);
|
|
@@ -551,6 +583,85 @@ function loadOrCreateConfig(projectDir, overrides) {
|
|
|
551
583
|
},
|
|
552
584
|
});
|
|
553
585
|
}
|
|
586
|
+
function handleAttest(args) {
|
|
587
|
+
let licenseKey;
|
|
588
|
+
let projectDir = process.cwd();
|
|
589
|
+
let findingId;
|
|
590
|
+
let status = "fixed";
|
|
591
|
+
let note = "";
|
|
592
|
+
for (let i = 0; i < args.length; i++) {
|
|
593
|
+
switch (args[i]) {
|
|
594
|
+
case "--license-key":
|
|
595
|
+
licenseKey = args[++i];
|
|
596
|
+
break;
|
|
597
|
+
case "--dir":
|
|
598
|
+
projectDir = (0, path_1.resolve)(args[++i]);
|
|
599
|
+
break;
|
|
600
|
+
case "--finding":
|
|
601
|
+
findingId = args[++i];
|
|
602
|
+
break;
|
|
603
|
+
case "--status":
|
|
604
|
+
status = args[++i];
|
|
605
|
+
break;
|
|
606
|
+
case "--note":
|
|
607
|
+
note = args[++i];
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// Env / config fallbacks for license key
|
|
612
|
+
if (!licenseKey)
|
|
613
|
+
licenseKey = process.env.NDC_LICENSE || process.env.NODATA_LICENSE_KEY || process.env.NODATA_API_KEY || process.env.NDC_API_KEY;
|
|
614
|
+
if (!licenseKey) {
|
|
615
|
+
try {
|
|
616
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
617
|
+
const configPath = require("path").join(home, ".nodata", "config.json");
|
|
618
|
+
if (require("fs").existsSync(configPath)) {
|
|
619
|
+
const config = JSON.parse(require("fs").readFileSync(configPath, "utf-8"));
|
|
620
|
+
if (config.api_key)
|
|
621
|
+
licenseKey = config.api_key;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch { /* ignore */ }
|
|
625
|
+
}
|
|
626
|
+
if (!licenseKey) {
|
|
627
|
+
console.error("\n \x1b[31m✗\x1b[0m No API key found. Pass --license-key or set NDC_LICENSE.\n");
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
if (!findingId) {
|
|
631
|
+
console.error("\n \x1b[31m✗\x1b[0m Missing --finding <id>.");
|
|
632
|
+
console.error(" Example finding IDs: PII_UNENCRYPTED_email, ROUTE_NO_AUTH, SECRET_POSTGRES_URI");
|
|
633
|
+
console.error("\n Usage:");
|
|
634
|
+
console.error(" npx nodata-guard attest --finding PII_UNENCRYPTED_email --status fixed --note \"Encrypted with pgcrypto\"");
|
|
635
|
+
console.error("\n Statuses: fixed | accepted_risk | not_applicable | compensating_control\n");
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
if (!note) {
|
|
639
|
+
console.error("\n \x1b[31m✗\x1b[0m Missing --note <description>. Explain what was done.\n");
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
const validStatuses = ["fixed", "accepted_risk", "not_applicable", "compensating_control"];
|
|
643
|
+
if (!validStatuses.includes(status)) {
|
|
644
|
+
console.error(`\n \x1b[31m✗\x1b[0m Invalid status "${status}". Use: ${validStatuses.join(" | ")}\n`);
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
(0, capsule_dir_1.attestFinding)(projectDir, licenseKey, findingId, status, note);
|
|
648
|
+
const statusColor = status === "fixed" ? "\x1b[32m" : "\x1b[33m";
|
|
649
|
+
console.log("");
|
|
650
|
+
console.log(" ╔══════════════════════════════════════╗");
|
|
651
|
+
console.log(" ║ NoData Guard — Attestation ║");
|
|
652
|
+
console.log(" ╚══════════════════════════════════════╝");
|
|
653
|
+
console.log("");
|
|
654
|
+
console.log(` Finding: ${findingId}`);
|
|
655
|
+
console.log(` Status: ${statusColor}${status}\x1b[0m`);
|
|
656
|
+
console.log(` Note: ${note}`);
|
|
657
|
+
console.log(` Signed: HMAC-SHA256 (license-key-bound)`);
|
|
658
|
+
console.log("");
|
|
659
|
+
console.log(" \x1b[32m✓\x1b[0m Saved to .capsule/overrides.json");
|
|
660
|
+
console.log(" \x1b[32m✓\x1b[0m Evidence logged to .capsule/evidence/");
|
|
661
|
+
console.log("");
|
|
662
|
+
console.log(" \x1b[2mCommit .capsule/ to git for audit trail.\x1b[0m");
|
|
663
|
+
console.log(" \x1b[2mRe-scan to see updated score.\x1b[0m\n");
|
|
664
|
+
}
|
|
554
665
|
function printHelp() {
|
|
555
666
|
console.log(`
|
|
556
667
|
NoData Guard v${VERSION} — Security Scanner + Recommendations
|
|
@@ -563,6 +674,17 @@ function printHelp() {
|
|
|
563
674
|
npx nodata-guard --license-key NDC-XXXX # Scan + recommend
|
|
564
675
|
npx nodata-guard --license-key NDC-XXXX --db $DATABASE_URL # Full scan (code + DB)
|
|
565
676
|
npx nodata-guard --license-key NDC-XXXX --schedule weekly # Setup CI schedule
|
|
677
|
+
npx nodata-guard status # Show .capsule/ status (no scan)
|
|
678
|
+
npx nodata-guard diff # Compare last 2 scans
|
|
679
|
+
npx nodata-guard attest --finding ID --status fixed --note "..." # Manual attestation
|
|
680
|
+
|
|
681
|
+
Subcommands:
|
|
682
|
+
status Show .capsule/ evidence (scores, proof, overrides) without scanning
|
|
683
|
+
diff Compare the last 2 scans — score delta, issues resolved/new
|
|
684
|
+
attest Manually attest a finding (saved to .capsule/overrides.json)
|
|
685
|
+
--finding <id> Finding ID (e.g., PII_UNENCRYPTED_email, ROUTE_NO_AUTH)
|
|
686
|
+
--status <status> fixed | accepted_risk | not_applicable | compensating_control
|
|
687
|
+
--note <text> Explanation of what was done
|
|
566
688
|
|
|
567
689
|
Scan Options:
|
|
568
690
|
--license-key <key> NoData license key (or set NDC_LICENSE env var)
|
|
@@ -618,6 +740,18 @@ function printHelp() {
|
|
|
618
740
|
# CI pipeline — fail on critical issues
|
|
619
741
|
npx nodata-guard --ci --fail-on critical
|
|
620
742
|
|
|
743
|
+
# Check .capsule/ status without scanning
|
|
744
|
+
npx nodata-guard status
|
|
745
|
+
|
|
746
|
+
# See what changed between last 2 scans
|
|
747
|
+
npx nodata-guard diff
|
|
748
|
+
|
|
749
|
+
# Attest that you fixed a finding
|
|
750
|
+
npx nodata-guard attest --finding PII_UNENCRYPTED_email --status fixed --note "Encrypted with pgcrypto in migration 042"
|
|
751
|
+
|
|
752
|
+
# Accept risk for a finding
|
|
753
|
+
npx nodata-guard attest --finding ROUTE_NO_AUTH --status accepted_risk --note "Public healthcheck endpoint, no auth needed"
|
|
754
|
+
|
|
621
755
|
Documentation: https://nodatacapsule.com/guard
|
|
622
756
|
`);
|
|
623
757
|
}
|
package/dist/code-scanner.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ interface FileEntry {
|
|
|
4
4
|
content: string;
|
|
5
5
|
}
|
|
6
6
|
export declare function readProjectFiles(projectDir: string, onProgress?: (count: number) => void): FileEntry[];
|
|
7
|
-
export declare function scanPIIFields(files: FileEntry[]): PIIFieldResult[];
|
|
7
|
+
export declare function scanPIIFields(files: FileEntry[], projectDir?: string): PIIFieldResult[];
|
|
8
8
|
export declare function scanRoutes(files: FileEntry[]): RouteResult[];
|
|
9
9
|
export declare function scanSecrets(files: FileEntry[]): SecretResult[];
|
|
10
10
|
export declare function detectStack(files: FileEntry[]): {
|
package/dist/code-scanner.js
CHANGED
|
@@ -18,6 +18,8 @@ exports.scanSecrets = scanSecrets;
|
|
|
18
18
|
exports.detectStack = detectStack;
|
|
19
19
|
const fs_1 = require("fs");
|
|
20
20
|
const path_1 = require("path");
|
|
21
|
+
const crypto_1 = require("crypto");
|
|
22
|
+
const capsule_dir_1 = require("./capsule-dir");
|
|
21
23
|
// ── File reading ──
|
|
22
24
|
const SKIP_DIRS = new Set([
|
|
23
25
|
"node_modules", ".git", ".next", ".nuxt", "dist", "build", "out",
|
|
@@ -97,7 +99,7 @@ function classifyColumn(col) {
|
|
|
97
99
|
}
|
|
98
100
|
return null;
|
|
99
101
|
}
|
|
100
|
-
function scanPIIFields(files) {
|
|
102
|
+
function scanPIIFields(files, projectDir) {
|
|
101
103
|
const fields = [];
|
|
102
104
|
const seen = new Set();
|
|
103
105
|
// Collect all column names to check for _encrypted companions
|
|
@@ -152,7 +154,7 @@ function scanPIIFields(files) {
|
|
|
152
154
|
const encryptFiles = files.filter(f => /createCipheriv|encrypt|decrypt|encryptPII|nodata_encrypt/i.test(f.content));
|
|
153
155
|
const proofFile = files.find(f => f.path.includes("nodata-proof.json"));
|
|
154
156
|
const triggerFiles = files.filter(f => f.path.endsWith(".sql") && /trg_encrypt_/i.test(f.content));
|
|
155
|
-
// Parse proof file
|
|
157
|
+
// Parse legacy proof file (nodata-proof.json)
|
|
156
158
|
const proofFields = new Set();
|
|
157
159
|
if (proofFile) {
|
|
158
160
|
try {
|
|
@@ -167,6 +169,27 @@ function scanPIIFields(files) {
|
|
|
167
169
|
}
|
|
168
170
|
catch { /* ignore */ }
|
|
169
171
|
}
|
|
172
|
+
// Parse .capsule/proof.json (DB-verified encryption from previous scans)
|
|
173
|
+
// This allows code-only scans to recognize encryption verified by a previous --db scan
|
|
174
|
+
if (projectDir) {
|
|
175
|
+
const capsuleProof = (0, capsule_dir_1.readProof)(projectDir);
|
|
176
|
+
if (capsuleProof && capsuleProof.fields.length > 0) {
|
|
177
|
+
// capsule proof uses hashed names — we need to build a lookup by hash
|
|
178
|
+
const fieldHashMap = new Map(); // hash → "table.column"
|
|
179
|
+
for (const field of fields) {
|
|
180
|
+
const tableHash = (0, crypto_1.createHash)("sha256").update(`capsule:name:${field.table}`).digest("hex").slice(0, 16);
|
|
181
|
+
const colHash = (0, crypto_1.createHash)("sha256").update(`capsule:name:${field.column}`).digest("hex").slice(0, 16);
|
|
182
|
+
fieldHashMap.set(`${tableHash}:${colHash}`, `${field.table}.${field.column}`);
|
|
183
|
+
}
|
|
184
|
+
for (const pf of capsuleProof.fields) {
|
|
185
|
+
const key = `${pf.table_hash}:${pf.column_hash}`;
|
|
186
|
+
const realName = fieldHashMap.get(key);
|
|
187
|
+
if (realName && pf.coverage_percent >= 50) {
|
|
188
|
+
proofFields.add(realName);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
170
193
|
// Parse trigger functions
|
|
171
194
|
const triggerTables = new Set();
|
|
172
195
|
for (const f of triggerFiles) {
|
package/dist/reporter.js
CHANGED
|
@@ -80,6 +80,13 @@ function generateReports(input) {
|
|
|
80
80
|
guard_version: GUARD_VERSION,
|
|
81
81
|
scores: {
|
|
82
82
|
overall: overallScore,
|
|
83
|
+
code: input.code ? Math.round((encCoverage * 0.5) + (routeProtection * 0.25) + (secretScore * 0.25)) : null,
|
|
84
|
+
db: input.db ? Math.round(((input.db.piiFields.length > 0
|
|
85
|
+
? Math.round((input.db.piiFields.filter(f => f.encrypted).length / input.db.piiFields.length) * 100)
|
|
86
|
+
: 100) * 0.6) +
|
|
87
|
+
((input.db.rls.length > 0
|
|
88
|
+
? Math.round((input.db.rls.filter(r => r.rls_enabled).length / input.db.rls.length) * 100)
|
|
89
|
+
: 0) * 0.4)) : null,
|
|
83
90
|
previous: input.previousScore,
|
|
84
91
|
improved,
|
|
85
92
|
delta: input.previousScore !== null ? overallScore - input.previousScore : 0,
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodatachat/guard",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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",
|