@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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export interface CapsuleProof {
|
|
2
|
+
version: "1.0";
|
|
3
|
+
updated_at: string;
|
|
4
|
+
fields: ProofField[];
|
|
5
|
+
}
|
|
6
|
+
export interface ProofField {
|
|
7
|
+
table_hash: string;
|
|
8
|
+
column_hash: string;
|
|
9
|
+
pattern: string;
|
|
10
|
+
sentinel_prefix: string;
|
|
11
|
+
row_count: number;
|
|
12
|
+
encrypted_count: number;
|
|
13
|
+
coverage_percent: number;
|
|
14
|
+
verified_at: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CapsuleOverride {
|
|
17
|
+
version: "1.0";
|
|
18
|
+
overrides: Override[];
|
|
19
|
+
}
|
|
20
|
+
export interface Override {
|
|
21
|
+
finding_id: string;
|
|
22
|
+
status: "fixed" | "accepted_risk" | "not_applicable" | "compensating_control";
|
|
23
|
+
note: string;
|
|
24
|
+
attested_by: string;
|
|
25
|
+
attested_at: string;
|
|
26
|
+
hmac: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ScoreSnapshot {
|
|
29
|
+
date: string;
|
|
30
|
+
score: number;
|
|
31
|
+
code_score: number | null;
|
|
32
|
+
db_score: number | null;
|
|
33
|
+
pii_total: number;
|
|
34
|
+
pii_encrypted: number;
|
|
35
|
+
coverage_percent: number;
|
|
36
|
+
findings_count: number;
|
|
37
|
+
critical: number;
|
|
38
|
+
high: number;
|
|
39
|
+
medium: number;
|
|
40
|
+
low: number;
|
|
41
|
+
scan_type: "code" | "full";
|
|
42
|
+
guard_version: string;
|
|
43
|
+
}
|
|
44
|
+
export interface EvidenceEntry {
|
|
45
|
+
date: string;
|
|
46
|
+
action: string;
|
|
47
|
+
category: string;
|
|
48
|
+
detail: string;
|
|
49
|
+
impact: string;
|
|
50
|
+
attested_by: string;
|
|
51
|
+
hmac: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Initialize .capsule/ directory. Called on every scan.
|
|
55
|
+
* Creates missing dirs/files, never overwrites existing data.
|
|
56
|
+
*/
|
|
57
|
+
export declare function initCapsuleDir(projectDir: string, licenseKey: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Save a score snapshot. Called after every scan.
|
|
60
|
+
*/
|
|
61
|
+
export declare function saveScore(capsuleDir: string, score: ScoreSnapshot): void;
|
|
62
|
+
/**
|
|
63
|
+
* Update proof.json with DB-verified encryption data.
|
|
64
|
+
* Called after DB scan detects encrypted fields.
|
|
65
|
+
*/
|
|
66
|
+
export declare function updateProof(capsuleDir: string, fields: Array<{
|
|
67
|
+
table: string;
|
|
68
|
+
column: string;
|
|
69
|
+
pattern: string;
|
|
70
|
+
sentinel_prefix: string;
|
|
71
|
+
row_count: number;
|
|
72
|
+
encrypted_count: number;
|
|
73
|
+
}>): void;
|
|
74
|
+
/**
|
|
75
|
+
* Add an evidence entry. Called after scan or manual attest.
|
|
76
|
+
*/
|
|
77
|
+
export declare function addEvidence(capsuleDir: string, licenseKey: string, entry: Omit<EvidenceEntry, "hmac">): void;
|
|
78
|
+
/**
|
|
79
|
+
* Read proof.json — used by code-scanner to check for known encryptions.
|
|
80
|
+
*/
|
|
81
|
+
export declare function readProof(projectDir: string): CapsuleProof | null;
|
|
82
|
+
/**
|
|
83
|
+
* Read overrides.json — used by reporter to adjust scores.
|
|
84
|
+
*/
|
|
85
|
+
export declare function readOverrides(projectDir: string): CapsuleOverride | null;
|
|
86
|
+
/**
|
|
87
|
+
* Get score history — used for trend display.
|
|
88
|
+
*/
|
|
89
|
+
export declare function getScoreHistory(projectDir: string): ScoreSnapshot[];
|
|
90
|
+
/**
|
|
91
|
+
* Print .capsule/ status to terminal.
|
|
92
|
+
*/
|
|
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;
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ═══════════════════════════════════════════════════════════
|
|
3
|
+
// .capsule/ Directory Manager
|
|
4
|
+
//
|
|
5
|
+
// Creates and maintains a local evidence directory in the customer's project.
|
|
6
|
+
// Everything stays local. NoData receives ONLY metadata (scores, counts).
|
|
7
|
+
//
|
|
8
|
+
// Structure:
|
|
9
|
+
// .capsule/
|
|
10
|
+
// ├── config.json ← Scanner settings, license, thresholds
|
|
11
|
+
// ├── proof.json ← Encryption sentinels, verified patterns
|
|
12
|
+
// ├── overrides.json ← Manual attestations ("I fixed this")
|
|
13
|
+
// ├── scores/
|
|
14
|
+
// │ └── YYYY-MM-DD.json ← Score snapshots per scan
|
|
15
|
+
// └── evidence/
|
|
16
|
+
// └── YYYY-MM-DD.json ← What was fixed, by whom, when
|
|
17
|
+
//
|
|
18
|
+
// Privacy:
|
|
19
|
+
// - No PII, table names, column names, or data values are stored
|
|
20
|
+
// - Sentinels are prefix-only (first 12 chars) — never full encrypted values
|
|
21
|
+
// - HMAC signature prevents tampering (proves the evidence is authentic)
|
|
22
|
+
// - NoData receives: score, date, finding_count, coverage_percent — NOTHING else
|
|
23
|
+
// ═══════════════════════════════════════════════════════════
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.initCapsuleDir = initCapsuleDir;
|
|
26
|
+
exports.saveScore = saveScore;
|
|
27
|
+
exports.updateProof = updateProof;
|
|
28
|
+
exports.addEvidence = addEvidence;
|
|
29
|
+
exports.readProof = readProof;
|
|
30
|
+
exports.readOverrides = readOverrides;
|
|
31
|
+
exports.getScoreHistory = getScoreHistory;
|
|
32
|
+
exports.printCapsuleStatus = printCapsuleStatus;
|
|
33
|
+
exports.attestFinding = attestFinding;
|
|
34
|
+
exports.printFullStatus = printFullStatus;
|
|
35
|
+
exports.printDiff = printDiff;
|
|
36
|
+
const path_1 = require("path");
|
|
37
|
+
const fs_1 = require("fs");
|
|
38
|
+
const crypto_1 = require("crypto");
|
|
39
|
+
const DIR_NAME = ".capsule";
|
|
40
|
+
const HMAC_KEY_DOMAIN = "capsule:evidence:integrity";
|
|
41
|
+
// ── Helpers ──
|
|
42
|
+
function hashName(name) {
|
|
43
|
+
return (0, crypto_1.createHash)("sha256").update(`capsule:name:${name}`).digest("hex").slice(0, 16);
|
|
44
|
+
}
|
|
45
|
+
function signEvidence(data, licenseKey) {
|
|
46
|
+
return (0, crypto_1.createHmac)("sha256", `${HMAC_KEY_DOMAIN}:${licenseKey}`)
|
|
47
|
+
.update(data)
|
|
48
|
+
.digest("hex")
|
|
49
|
+
.slice(0, 32);
|
|
50
|
+
}
|
|
51
|
+
function ensureDir(dir) {
|
|
52
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
53
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
// ── Public API ──
|
|
56
|
+
/**
|
|
57
|
+
* Initialize .capsule/ directory. Called on every scan.
|
|
58
|
+
* Creates missing dirs/files, never overwrites existing data.
|
|
59
|
+
*/
|
|
60
|
+
function initCapsuleDir(projectDir, licenseKey) {
|
|
61
|
+
const capsuleDir = (0, path_1.resolve)(projectDir, DIR_NAME);
|
|
62
|
+
ensureDir(capsuleDir);
|
|
63
|
+
ensureDir((0, path_1.join)(capsuleDir, "scores"));
|
|
64
|
+
ensureDir((0, path_1.join)(capsuleDir, "evidence"));
|
|
65
|
+
// Config — create only if missing
|
|
66
|
+
const configPath = (0, path_1.join)(capsuleDir, "config.json");
|
|
67
|
+
if (!(0, fs_1.existsSync)(configPath)) {
|
|
68
|
+
(0, fs_1.writeFileSync)(configPath, JSON.stringify({
|
|
69
|
+
version: "1.0",
|
|
70
|
+
created_at: new Date().toISOString(),
|
|
71
|
+
license_prefix: licenseKey.slice(0, 12) + "...",
|
|
72
|
+
thresholds: {
|
|
73
|
+
fail_on: "critical",
|
|
74
|
+
target_score: 85,
|
|
75
|
+
encryption_target: 90,
|
|
76
|
+
},
|
|
77
|
+
scan_schedule: "manual",
|
|
78
|
+
notes: "This directory contains local evidence for SOC compliance. Commit to git for audit history. NoData never receives this data.",
|
|
79
|
+
}, null, 2), "utf-8");
|
|
80
|
+
}
|
|
81
|
+
// Proof — create only if missing
|
|
82
|
+
const proofPath = (0, path_1.join)(capsuleDir, "proof.json");
|
|
83
|
+
if (!(0, fs_1.existsSync)(proofPath)) {
|
|
84
|
+
const proof = { version: "1.0", updated_at: new Date().toISOString(), fields: [] };
|
|
85
|
+
(0, fs_1.writeFileSync)(proofPath, JSON.stringify(proof, null, 2), "utf-8");
|
|
86
|
+
}
|
|
87
|
+
// Overrides — create only if missing
|
|
88
|
+
const overridesPath = (0, path_1.join)(capsuleDir, "overrides.json");
|
|
89
|
+
if (!(0, fs_1.existsSync)(overridesPath)) {
|
|
90
|
+
const overrides = { version: "1.0", overrides: [] };
|
|
91
|
+
(0, fs_1.writeFileSync)(overridesPath, JSON.stringify(overrides, null, 2), "utf-8");
|
|
92
|
+
}
|
|
93
|
+
// Ensure .gitignore does NOT exclude .capsule/
|
|
94
|
+
// (It should be committed for audit trail)
|
|
95
|
+
const gitignorePath = (0, path_1.resolve)(projectDir, ".gitignore");
|
|
96
|
+
if ((0, fs_1.existsSync)(gitignorePath)) {
|
|
97
|
+
const content = (0, fs_1.readFileSync)(gitignorePath, "utf-8");
|
|
98
|
+
if (content.includes(".capsule")) {
|
|
99
|
+
// Remove .capsule from gitignore if present — it should be tracked
|
|
100
|
+
const lines = content.split("\n").filter(l => !l.trim().startsWith(".capsule"));
|
|
101
|
+
(0, fs_1.writeFileSync)(gitignorePath, lines.join("\n"), "utf-8");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return capsuleDir;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Save a score snapshot. Called after every scan.
|
|
108
|
+
*/
|
|
109
|
+
function saveScore(capsuleDir, score) {
|
|
110
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
111
|
+
const scoresDir = (0, path_1.join)(capsuleDir, "scores");
|
|
112
|
+
ensureDir(scoresDir);
|
|
113
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(scoresDir, `${date}.json`), JSON.stringify(score, null, 2), "utf-8");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Update proof.json with DB-verified encryption data.
|
|
117
|
+
* Called after DB scan detects encrypted fields.
|
|
118
|
+
*/
|
|
119
|
+
function updateProof(capsuleDir, fields) {
|
|
120
|
+
const proofPath = (0, path_1.join)(capsuleDir, "proof.json");
|
|
121
|
+
const proof = (0, fs_1.existsSync)(proofPath)
|
|
122
|
+
? JSON.parse((0, fs_1.readFileSync)(proofPath, "utf-8"))
|
|
123
|
+
: { version: "1.0", updated_at: "", fields: [] };
|
|
124
|
+
for (const f of fields) {
|
|
125
|
+
const tableHash = hashName(f.table);
|
|
126
|
+
const columnHash = hashName(f.column);
|
|
127
|
+
const coverage = f.row_count > 0 ? Math.round((f.encrypted_count / f.row_count) * 100) : 0;
|
|
128
|
+
// Update existing or add new
|
|
129
|
+
const existing = proof.fields.find(p => p.table_hash === tableHash && p.column_hash === columnHash);
|
|
130
|
+
if (existing) {
|
|
131
|
+
existing.pattern = f.pattern;
|
|
132
|
+
existing.sentinel_prefix = f.sentinel_prefix.slice(0, 12);
|
|
133
|
+
existing.row_count = f.row_count;
|
|
134
|
+
existing.encrypted_count = f.encrypted_count;
|
|
135
|
+
existing.coverage_percent = coverage;
|
|
136
|
+
existing.verified_at = new Date().toISOString();
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
proof.fields.push({
|
|
140
|
+
table_hash: tableHash,
|
|
141
|
+
column_hash: columnHash,
|
|
142
|
+
pattern: f.pattern,
|
|
143
|
+
sentinel_prefix: f.sentinel_prefix.slice(0, 12),
|
|
144
|
+
row_count: f.row_count,
|
|
145
|
+
encrypted_count: f.encrypted_count,
|
|
146
|
+
coverage_percent: coverage,
|
|
147
|
+
verified_at: new Date().toISOString(),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
proof.updated_at = new Date().toISOString();
|
|
152
|
+
(0, fs_1.writeFileSync)(proofPath, JSON.stringify(proof, null, 2), "utf-8");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Add an evidence entry. Called after scan or manual attest.
|
|
156
|
+
*/
|
|
157
|
+
function addEvidence(capsuleDir, licenseKey, entry) {
|
|
158
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
159
|
+
const evidenceDir = (0, path_1.join)(capsuleDir, "evidence");
|
|
160
|
+
ensureDir(evidenceDir);
|
|
161
|
+
const filePath = (0, path_1.join)(evidenceDir, `${date}.json`);
|
|
162
|
+
const entries = (0, fs_1.existsSync)(filePath)
|
|
163
|
+
? JSON.parse((0, fs_1.readFileSync)(filePath, "utf-8"))
|
|
164
|
+
: [];
|
|
165
|
+
// Sign the entry — proves it wasn't tampered with
|
|
166
|
+
const payload = JSON.stringify({ ...entry });
|
|
167
|
+
const hmac = signEvidence(payload, licenseKey);
|
|
168
|
+
entries.push({ ...entry, hmac });
|
|
169
|
+
(0, fs_1.writeFileSync)(filePath, JSON.stringify(entries, null, 2), "utf-8");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Read proof.json — used by code-scanner to check for known encryptions.
|
|
173
|
+
*/
|
|
174
|
+
function readProof(projectDir) {
|
|
175
|
+
const proofPath = (0, path_1.join)(projectDir, DIR_NAME, "proof.json");
|
|
176
|
+
if (!(0, fs_1.existsSync)(proofPath))
|
|
177
|
+
return null;
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse((0, fs_1.readFileSync)(proofPath, "utf-8"));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Read overrides.json — used by reporter to adjust scores.
|
|
187
|
+
*/
|
|
188
|
+
function readOverrides(projectDir) {
|
|
189
|
+
const overridesPath = (0, path_1.join)(projectDir, DIR_NAME, "overrides.json");
|
|
190
|
+
if (!(0, fs_1.existsSync)(overridesPath))
|
|
191
|
+
return null;
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse((0, fs_1.readFileSync)(overridesPath, "utf-8"));
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get score history — used for trend display.
|
|
201
|
+
*/
|
|
202
|
+
function getScoreHistory(projectDir) {
|
|
203
|
+
const scoresDir = (0, path_1.join)(projectDir, DIR_NAME, "scores");
|
|
204
|
+
if (!(0, fs_1.existsSync)(scoresDir))
|
|
205
|
+
return [];
|
|
206
|
+
const files = (0, fs_1.readdirSync)(scoresDir)
|
|
207
|
+
.filter((f) => f.endsWith(".json"))
|
|
208
|
+
.sort();
|
|
209
|
+
return files.map((f) => {
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(scoresDir, f), "utf-8"));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}).filter(Boolean);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Print .capsule/ status to terminal.
|
|
220
|
+
*/
|
|
221
|
+
function printCapsuleStatus(projectDir, ciMode) {
|
|
222
|
+
const capsuleDir = (0, path_1.join)(projectDir, DIR_NAME);
|
|
223
|
+
if (!(0, fs_1.existsSync)(capsuleDir))
|
|
224
|
+
return;
|
|
225
|
+
if (ciMode)
|
|
226
|
+
return;
|
|
227
|
+
const history = getScoreHistory(projectDir);
|
|
228
|
+
const proof = readProof(projectDir);
|
|
229
|
+
const overrides = readOverrides(projectDir);
|
|
230
|
+
console.log("");
|
|
231
|
+
console.log(" ══════════════════════════════════════");
|
|
232
|
+
console.log(" .capsule/ LOCAL EVIDENCE");
|
|
233
|
+
console.log(" ══════════════════════════════════════");
|
|
234
|
+
console.log(` Score history: ${history.length} scan(s)`);
|
|
235
|
+
if (history.length >= 2) {
|
|
236
|
+
const first = history[0];
|
|
237
|
+
const last = history[history.length - 1];
|
|
238
|
+
const delta = last.score - first.score;
|
|
239
|
+
const arrow = delta > 0 ? `\x1b[32m↑${delta}%\x1b[0m` : delta < 0 ? `\x1b[31m↓${Math.abs(delta)}%\x1b[0m` : "→ no change";
|
|
240
|
+
console.log(` Trend: ${first.score}% → ${last.score}% ${arrow}`);
|
|
241
|
+
}
|
|
242
|
+
if (proof) {
|
|
243
|
+
console.log(` Proven fields: ${proof.fields.length} (DB-verified encryption)`);
|
|
244
|
+
}
|
|
245
|
+
if (overrides && overrides.overrides.length > 0) {
|
|
246
|
+
console.log(` Overrides: ${overrides.overrides.length} manual attestation(s)`);
|
|
247
|
+
}
|
|
248
|
+
console.log(" ──────────────────────────────────────");
|
|
249
|
+
console.log(" \x1b[2mAll evidence stays local. Commit to git for audit trail.\x1b[0m");
|
|
250
|
+
console.log(" ══════════════════════════════════════\n");
|
|
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
|
+
}
|