@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/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 summary NO VALUES, just table.column + status
106
+ // Per-field: hashed identifier + type + status NO real names
97
107
  fields: input.code.piiFields.map(f => ({
98
- table: f.table,
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, // each plaintext field has a recommendation
134
+ recommendations_available: plaintextPII,
127
135
  },
128
- // Fix suggestionssafe to send (just recommendation text, no code/values)
136
+ // REDACTED findingsgeneric 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: `${f.table}.${f.column} ${f.pii_type} stored in plaintext`,
135
- fix_suggestion: `Add AES-256-GCM encryption to ${f.table}.${f.column} via Capsule field permissions`,
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 ${s.file}:${s.line}`,
143
- fix_suggestion: `Move to .env and encrypt with @nodatachat/protect`,
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(r => ({
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: `Unprotected route: ${r.path}`,
151
- fix_suggestion: `Add authentication middleware (withAuth or API key check)`,
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
- table: string;
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,6 @@
1
+ /** Encrypt plaintext string → base64 blob + hex IV + hex salt */
2
+ export declare function encryptForVault(pin: string, plaintext: string): {
3
+ ciphertext: string;
4
+ iv: string;
5
+ salt: string;
6
+ };
@@ -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.2.0",
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",