@nodatachat/guard 2.2.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 +104 -0
- package/dist/capsule-dir.js +465 -0
- package/dist/cli.js +327 -4
- package/dist/code-scanner.d.ts +1 -1
- package/dist/code-scanner.js +27 -4
- package/dist/db-scanner.js +22 -5
- package/dist/reporter.js +52 -13
- package/dist/types.d.ts +16 -3
- package/dist/vault-crypto.d.ts +6 -0
- package/dist/vault-crypto.js +36 -0
- package/package.json +1 -1
package/dist/reporter.js
CHANGED
|
@@ -67,6 +67,9 @@ function generateReports(input) {
|
|
|
67
67
|
const proofData = allPII.map(f => `${f.table}.${f.column}:${f.encrypted}`).join("|") + `|${overallScore}|${now}`;
|
|
68
68
|
const proofHash = (0, crypto_1.createHash)("sha256").update(proofData).digest("hex");
|
|
69
69
|
// ── Build metadata report (what we send) ──
|
|
70
|
+
// PRIVACY DOCTRINE: No structural metadata (table/column/file names) ever leaves the machine.
|
|
71
|
+
// We send ONLY: counts, scores, severity, generic category labels, SOC control IDs.
|
|
72
|
+
// The customer keeps the full report locally and can verify the diff.
|
|
70
73
|
const metadata = {
|
|
71
74
|
version: "1.0",
|
|
72
75
|
scan_id: input.scanId,
|
|
@@ -77,6 +80,13 @@ function generateReports(input) {
|
|
|
77
80
|
guard_version: GUARD_VERSION,
|
|
78
81
|
scores: {
|
|
79
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,
|
|
80
90
|
previous: input.previousScore,
|
|
81
91
|
improved,
|
|
82
92
|
delta: input.previousScore !== null ? overallScore - input.previousScore : 0,
|
|
@@ -93,13 +103,11 @@ function generateReports(input) {
|
|
|
93
103
|
route_protection_percent: routeProtection,
|
|
94
104
|
secrets_found: (input.code.secrets.filter(s => !s.is_env_interpolated)).length,
|
|
95
105
|
secrets_critical: criticalSecrets,
|
|
96
|
-
// Per-field
|
|
106
|
+
// Per-field: hashed identifier + type + status — NO real names
|
|
97
107
|
fields: input.code.piiFields.map(f => ({
|
|
98
|
-
|
|
99
|
-
column: f.column,
|
|
108
|
+
field_hash: (0, crypto_1.createHash)("sha256").update(`${f.table}.${f.column}`).digest("hex").slice(0, 16),
|
|
100
109
|
pii_type: f.pii_type,
|
|
101
110
|
encrypted: f.encrypted,
|
|
102
|
-
pattern: f.encryption_pattern,
|
|
103
111
|
})),
|
|
104
112
|
} : null,
|
|
105
113
|
db_summary: input.db ? {
|
|
@@ -123,32 +131,32 @@ function generateReports(input) {
|
|
|
123
131
|
critical: criticalSecrets,
|
|
124
132
|
high: highSecrets + plaintextPII,
|
|
125
133
|
medium: medSecrets,
|
|
126
|
-
recommendations_available: plaintextPII,
|
|
134
|
+
recommendations_available: plaintextPII,
|
|
127
135
|
},
|
|
128
|
-
//
|
|
136
|
+
// REDACTED findings — generic titles only, no structural metadata
|
|
129
137
|
findings: [
|
|
130
138
|
...allPII.filter(f => !f.encrypted).map(f => ({
|
|
131
139
|
severity: "high",
|
|
132
140
|
category: "pii",
|
|
133
141
|
rule_id: `PII_${f.pii_type.toUpperCase()}`,
|
|
134
|
-
title:
|
|
135
|
-
fix_suggestion:
|
|
142
|
+
title: `PII field (${f.pii_type}) stored unencrypted`,
|
|
143
|
+
fix_suggestion: "Add AES-256-GCM column encryption via Capsule Protect",
|
|
136
144
|
soc_control: "CC6.7",
|
|
137
145
|
})),
|
|
138
146
|
...(input.code?.secrets.filter(s => !s.is_env_interpolated) || []).map(s => ({
|
|
139
147
|
severity: s.severity,
|
|
140
148
|
category: "secrets",
|
|
141
149
|
rule_id: `SECRET_${s.type.toUpperCase().replace(/-/g, "_")}`,
|
|
142
|
-
title: `Hardcoded ${s.type} in
|
|
143
|
-
fix_suggestion:
|
|
150
|
+
title: `Hardcoded secret (${s.type}) in source code`,
|
|
151
|
+
fix_suggestion: "Move to .env and encrypt with @nodatachat/protect",
|
|
144
152
|
soc_control: "CC6.1",
|
|
145
153
|
})),
|
|
146
|
-
...(input.code?.routes.filter(r => !r.has_auth) || []).slice(0, 20).map(
|
|
154
|
+
...(input.code?.routes.filter(r => !r.has_auth) || []).slice(0, 20).map(() => ({
|
|
147
155
|
severity: "high",
|
|
148
156
|
category: "auth",
|
|
149
157
|
rule_id: "ROUTE_NO_AUTH",
|
|
150
|
-
title:
|
|
151
|
-
fix_suggestion:
|
|
158
|
+
title: "API route missing authentication",
|
|
159
|
+
fix_suggestion: "Add authentication middleware (withAuth or API key check)",
|
|
152
160
|
soc_control: "CC6.3",
|
|
153
161
|
})),
|
|
154
162
|
],
|
|
@@ -159,6 +167,7 @@ function generateReports(input) {
|
|
|
159
167
|
contains_credentials: false,
|
|
160
168
|
contains_source_code: false,
|
|
161
169
|
contains_file_contents: false,
|
|
170
|
+
contains_structural_metadata: false,
|
|
162
171
|
customer_can_verify: true,
|
|
163
172
|
},
|
|
164
173
|
};
|
|
@@ -207,6 +216,36 @@ function generateReports(input) {
|
|
|
207
216
|
},
|
|
208
217
|
proof_hash: proofHash,
|
|
209
218
|
metadata_preview: metadata,
|
|
219
|
+
// ── HOW TO FIX ──
|
|
220
|
+
// This section stays in the LOCAL report only. Never sent to NoData.
|
|
221
|
+
how_to_fix: {
|
|
222
|
+
summary: `This scan found ${plaintextPII} unencrypted PII fields, ${criticalSecrets} hardcoded secrets, and ${totalRoutes - protectedRoutes} unprotected routes.`,
|
|
223
|
+
steps: [
|
|
224
|
+
...(plaintextPII > 0 ? [{
|
|
225
|
+
priority: 1,
|
|
226
|
+
action: "Encrypt PII fields",
|
|
227
|
+
detail: `${plaintextPII} fields contain personal data without encryption. Each field listed below needs AES-256-GCM column-level encryption.`,
|
|
228
|
+
fields: allPII.filter(f => !f.encrypted).map(f => `${f.table}.${f.column} (${f.pii_type})`),
|
|
229
|
+
impact: `+${Math.min(Math.round(plaintextPII * 1.5), 30)}% score improvement`,
|
|
230
|
+
}] : []),
|
|
231
|
+
...(criticalSecrets > 0 ? [{
|
|
232
|
+
priority: 2,
|
|
233
|
+
action: "Remove hardcoded secrets",
|
|
234
|
+
detail: `${criticalSecrets} secrets are hardcoded in source files. Move them to .env and encrypt with: npx @nodatachat/protect init`,
|
|
235
|
+
fields: (input.code?.secrets.filter(s => !s.is_env_interpolated && s.severity === "critical") || []).map(s => `${s.file}:${s.line} (${s.type})`),
|
|
236
|
+
impact: `+${Math.min(criticalSecrets * 5, 25)}% score improvement`,
|
|
237
|
+
}] : []),
|
|
238
|
+
...((totalRoutes - protectedRoutes > 0) ? [{
|
|
239
|
+
priority: 3,
|
|
240
|
+
action: "Add route authentication",
|
|
241
|
+
detail: `${totalRoutes - protectedRoutes} API routes have no auth middleware. Add withAuth, API key validation, or rate limiting.`,
|
|
242
|
+
fields: (input.code?.routes.filter(r => !r.has_auth) || []).map(r => r.path),
|
|
243
|
+
impact: `+${Math.min(Math.round((totalRoutes - protectedRoutes) * 0.5), 15)}% score improvement`,
|
|
244
|
+
}] : []),
|
|
245
|
+
],
|
|
246
|
+
workflow: "Give this report to your development team or security engineer. They can review each finding and implement the fix suggestions. After fixing, re-run Guard to verify your score improved.",
|
|
247
|
+
re_scan: `npx @nodatachat/guard@latest --license-key YOUR_KEY`,
|
|
248
|
+
},
|
|
210
249
|
};
|
|
211
250
|
return { full, metadata };
|
|
212
251
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -88,6 +88,18 @@ export interface FullReport {
|
|
|
88
88
|
};
|
|
89
89
|
proof_hash: string;
|
|
90
90
|
metadata_preview: MetadataReport;
|
|
91
|
+
how_to_fix: {
|
|
92
|
+
summary: string;
|
|
93
|
+
steps: Array<{
|
|
94
|
+
priority: number;
|
|
95
|
+
action: string;
|
|
96
|
+
detail: string;
|
|
97
|
+
fields: string[];
|
|
98
|
+
impact: string;
|
|
99
|
+
}>;
|
|
100
|
+
workflow: string;
|
|
101
|
+
re_scan: string;
|
|
102
|
+
};
|
|
91
103
|
}
|
|
92
104
|
export interface MetadataReport {
|
|
93
105
|
version: "1.0";
|
|
@@ -99,6 +111,8 @@ export interface MetadataReport {
|
|
|
99
111
|
guard_version: string;
|
|
100
112
|
scores: {
|
|
101
113
|
overall: number;
|
|
114
|
+
code: number | null;
|
|
115
|
+
db: number | null;
|
|
102
116
|
previous: number | null;
|
|
103
117
|
improved: boolean;
|
|
104
118
|
delta: number;
|
|
@@ -116,11 +130,9 @@ export interface MetadataReport {
|
|
|
116
130
|
secrets_found: number;
|
|
117
131
|
secrets_critical: number;
|
|
118
132
|
fields: Array<{
|
|
119
|
-
|
|
120
|
-
column: string;
|
|
133
|
+
field_hash: string;
|
|
121
134
|
pii_type: string;
|
|
122
135
|
encrypted: boolean;
|
|
123
|
-
pattern: string | null;
|
|
124
136
|
}>;
|
|
125
137
|
} | null;
|
|
126
138
|
db_summary: {
|
|
@@ -157,6 +169,7 @@ export interface MetadataReport {
|
|
|
157
169
|
contains_credentials: false;
|
|
158
170
|
contains_source_code: false;
|
|
159
171
|
contains_file_contents: false;
|
|
172
|
+
contains_structural_metadata: false;
|
|
160
173
|
customer_can_verify: true;
|
|
161
174
|
};
|
|
162
175
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ══════════════════════════════════════════════════════════════
|
|
3
|
+
// vault-crypto.ts — Node.js AES-256-GCM encryption for Guard CLI
|
|
4
|
+
//
|
|
5
|
+
// Mirrors the browser vault-crypto.ts exactly:
|
|
6
|
+
// PIN → PBKDF2 (100k iterations, SHA-256) → AES-256-GCM key
|
|
7
|
+
//
|
|
8
|
+
// The encrypted blob produced here can be decrypted by the
|
|
9
|
+
// browser VaultReportViewer using the same PIN.
|
|
10
|
+
// ══════════════════════════════════════════════════════════════
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.encryptForVault = encryptForVault;
|
|
13
|
+
const crypto_1 = require("crypto");
|
|
14
|
+
const ITERATIONS = 100_000;
|
|
15
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
16
|
+
const SALT_LENGTH = 16; // bytes
|
|
17
|
+
const IV_LENGTH = 12; // bytes (standard for AES-GCM)
|
|
18
|
+
/** Encrypt plaintext string → base64 blob + hex IV + hex salt */
|
|
19
|
+
function encryptForVault(pin, plaintext) {
|
|
20
|
+
const salt = (0, crypto_1.randomBytes)(SALT_LENGTH);
|
|
21
|
+
const iv = (0, crypto_1.randomBytes)(IV_LENGTH);
|
|
22
|
+
const key = (0, crypto_1.pbkdf2Sync)(pin, salt, ITERATIONS, KEY_LENGTH, "sha256");
|
|
23
|
+
const cipher = (0, crypto_1.createCipheriv)("aes-256-gcm", key, iv);
|
|
24
|
+
const encrypted = Buffer.concat([
|
|
25
|
+
cipher.update(plaintext, "utf8"),
|
|
26
|
+
cipher.final(),
|
|
27
|
+
]);
|
|
28
|
+
const authTag = cipher.getAuthTag();
|
|
29
|
+
// Combine ciphertext + authTag (browser Web Crypto expects them concatenated)
|
|
30
|
+
const combined = Buffer.concat([encrypted, authTag]);
|
|
31
|
+
return {
|
|
32
|
+
ciphertext: combined.toString("base64"),
|
|
33
|
+
iv: iv.toString("hex"),
|
|
34
|
+
salt: salt.toString("hex"),
|
|
35
|
+
};
|
|
36
|
+
}
|
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",
|