opena2a-cli 0.1.2 → 0.2.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.
Files changed (63) hide show
  1. package/README.md +225 -1
  2. package/dist/commands/guard-hooks.d.ts +27 -0
  3. package/dist/commands/guard-hooks.d.ts.map +1 -0
  4. package/dist/commands/guard-hooks.js +207 -0
  5. package/dist/commands/guard-hooks.js.map +1 -0
  6. package/dist/commands/guard-policy.d.ts +54 -0
  7. package/dist/commands/guard-policy.d.ts.map +1 -0
  8. package/dist/commands/guard-policy.js +251 -0
  9. package/dist/commands/guard-policy.js.map +1 -0
  10. package/dist/commands/guard-signing.d.ts +52 -0
  11. package/dist/commands/guard-signing.d.ts.map +1 -0
  12. package/dist/commands/guard-signing.js +185 -0
  13. package/dist/commands/guard-signing.js.map +1 -0
  14. package/dist/commands/guard-snapshots.d.ts +54 -0
  15. package/dist/commands/guard-snapshots.d.ts.map +1 -0
  16. package/dist/commands/guard-snapshots.js +346 -0
  17. package/dist/commands/guard-snapshots.js.map +1 -0
  18. package/dist/commands/guard.d.ts +60 -4
  19. package/dist/commands/guard.d.ts.map +1 -1
  20. package/dist/commands/guard.js +475 -95
  21. package/dist/commands/guard.js.map +1 -1
  22. package/dist/commands/init.js +3 -4
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/shield.d.ts +3 -0
  25. package/dist/commands/shield.d.ts.map +1 -1
  26. package/dist/commands/shield.js +458 -30
  27. package/dist/commands/shield.js.map +1 -1
  28. package/dist/index.js +15 -6
  29. package/dist/index.js.map +1 -1
  30. package/dist/router.d.ts.map +1 -1
  31. package/dist/router.js +1 -0
  32. package/dist/router.js.map +1 -1
  33. package/dist/shield/arp-bridge.d.ts +62 -0
  34. package/dist/shield/arp-bridge.d.ts.map +1 -0
  35. package/dist/shield/arp-bridge.js +198 -0
  36. package/dist/shield/arp-bridge.js.map +1 -0
  37. package/dist/shield/baselines.d.ts +58 -0
  38. package/dist/shield/baselines.d.ts.map +1 -0
  39. package/dist/shield/baselines.js +371 -0
  40. package/dist/shield/baselines.js.map +1 -0
  41. package/dist/shield/findings.d.ts +52 -0
  42. package/dist/shield/findings.d.ts.map +1 -0
  43. package/dist/shield/findings.js +336 -0
  44. package/dist/shield/findings.js.map +1 -0
  45. package/dist/shield/integrity.d.ts.map +1 -1
  46. package/dist/shield/integrity.js +6 -2
  47. package/dist/shield/integrity.js.map +1 -1
  48. package/dist/shield/report-html.d.ts +29 -0
  49. package/dist/shield/report-html.d.ts.map +1 -0
  50. package/dist/shield/report-html.js +596 -0
  51. package/dist/shield/report-html.js.map +1 -0
  52. package/dist/shield/sarif.d.ts +65 -0
  53. package/dist/shield/sarif.d.ts.map +1 -0
  54. package/dist/shield/sarif.js +108 -0
  55. package/dist/shield/sarif.js.map +1 -0
  56. package/dist/shield/status.d.ts.map +1 -1
  57. package/dist/shield/status.js +6 -6
  58. package/dist/shield/status.js.map +1 -1
  59. package/dist/shield/types.d.ts +19 -1
  60. package/dist/shield/types.d.ts.map +1 -1
  61. package/dist/shield/types.js +2 -1
  62. package/dist/shield/types.js.map +1 -1
  63. package/package.json +1 -1
@@ -3,9 +3,15 @@
3
3
  * opena2a guard -- ConfigGuard: config file integrity signing and verification.
4
4
  *
5
5
  * Subcommands:
6
- * - sign: Hash all detected config files, store in signatures.json
7
- * - verify: Check all signed files for tampering (hash mismatch)
8
- * - status: Summary of signed, unsigned, and tampered files
6
+ * - sign: Hash all detected config files, store in signatures.json
7
+ * - verify: Check all signed files for tampering (hash mismatch)
8
+ * - status: Summary of signed, unsigned, and tampered files
9
+ * - watch: Monitor signed files for changes in real-time
10
+ * - diff: Show detailed changes between current files and signed baseline
11
+ * - policy: Manage guard policy (signing requirements, heartbeat disable)
12
+ * - hook: Install/uninstall git pre-commit hook for automatic verification
13
+ * - resign: Re-sign modified files after confirming changes are intentional
14
+ * - snapshot: Create, list, or restore timestamped signature snapshots
9
15
  */
10
16
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
17
  if (k2 === undefined) k2 = k;
@@ -43,6 +49,7 @@ var __importStar = (this && this.__importStar) || (function () {
43
49
  Object.defineProperty(exports, "__esModule", { value: true });
44
50
  exports._internals = void 0;
45
51
  exports.guard = guard;
52
+ exports.verifyConfigIntegrity = verifyConfigIntegrity;
46
53
  const fs = __importStar(require("node:fs"));
47
54
  const os = __importStar(require("node:os"));
48
55
  const path = __importStar(require("node:path"));
@@ -62,6 +69,21 @@ const GUARD_FILES = [
62
69
  ];
63
70
  const STORE_DIR = '.opena2a/guard';
64
71
  const STORE_FILE = 'signatures.json';
72
+ const EXIT_QUARANTINE = 3;
73
+ // --- Event emission ---
74
+ async function emitEvent(category, action, target, severity, outcome, detail) {
75
+ try {
76
+ const { writeEvent } = await import('../shield/events.js');
77
+ writeEvent({
78
+ source: 'configguard', category, severity,
79
+ agent: null, sessionId: null, action, target, outcome, detail,
80
+ orgId: null, managed: false, agentId: null,
81
+ });
82
+ }
83
+ catch {
84
+ // Shield module not available
85
+ }
86
+ }
65
87
  // --- Core ---
66
88
  async function guard(options) {
67
89
  const targetDir = path.resolve(options.targetDir ?? process.cwd());
@@ -70,15 +92,32 @@ async function guard(options) {
70
92
  return 1;
71
93
  }
72
94
  switch (options.subcommand) {
73
- case 'sign':
74
- return guardSign(targetDir, options);
75
- case 'verify':
76
- return guardVerify(targetDir, options);
77
- case 'status':
78
- return guardStatus(targetDir, options);
95
+ case 'sign': return guardSign(targetDir, options);
96
+ case 'verify': return guardVerify(targetDir, options);
97
+ case 'status': return guardStatus(targetDir, options);
98
+ case 'watch': return guardWatch(targetDir, options);
99
+ case 'diff': return guardDiff(targetDir, options);
100
+ case 'policy': {
101
+ const { guardPolicy } = await import('./guard-policy.js');
102
+ const action = options.args?.[0] ?? 'show';
103
+ return guardPolicy(targetDir, action, { format: options.format });
104
+ }
105
+ case 'hook': {
106
+ const { guardHook } = await import('./guard-hooks.js');
107
+ const action = options.args?.[0] ?? '';
108
+ return guardHook(action, targetDir);
109
+ }
110
+ case 'resign': {
111
+ const { guardResign: resign } = await import('./guard-snapshots.js');
112
+ return resign(targetDir, options);
113
+ }
114
+ case 'snapshot': {
115
+ const { guardSnapshot: snapshot } = await import('./guard-snapshots.js');
116
+ return snapshot(targetDir, options);
117
+ }
79
118
  default:
80
119
  process.stderr.write((0, colors_js_1.red)(`Unknown subcommand: ${options.subcommand}\n`));
81
- process.stderr.write('Usage: opena2a guard <sign|verify|status>\n');
120
+ process.stderr.write('Usage: opena2a guard <sign|verify|status|watch|diff|policy|hook|resign|snapshot>\n');
82
121
  return 1;
83
122
  }
84
123
  }
@@ -97,17 +136,11 @@ async function guardSign(targetDir, options) {
97
136
  const content = fs.readFileSync(fullPath);
98
137
  const hash = 'sha256:' + (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
99
138
  const stat = fs.statSync(fullPath);
100
- signatures.push({
101
- filePath: relPath,
102
- hash,
103
- signedAt: new Date().toISOString(),
104
- signedBy: os.userInfo().username + '@opena2a-cli',
105
- fileSize: stat.size,
106
- });
139
+ signatures.push({ filePath: relPath, hash, signedAt: new Date().toISOString(), signedBy: os.userInfo().username + '@opena2a-cli', fileSize: stat.size });
107
140
  }
108
141
  if (!isJson)
109
142
  spinner.stop();
110
- if (signatures.length === 0) {
143
+ if (signatures.length === 0 && !options.skills && !options.heartbeats) {
111
144
  if (isJson) {
112
145
  process.stdout.write(JSON.stringify({ signed: 0, files: [] }, null, 2) + '\n');
113
146
  }
@@ -116,30 +149,60 @@ async function guardSign(targetDir, options) {
116
149
  }
117
150
  return 0;
118
151
  }
119
- // Write signature store
120
- const store = {
121
- version: 1,
122
- signatures,
123
- updatedAt: new Date().toISOString(),
124
- };
125
- const storeDir = path.join(targetDir, STORE_DIR);
126
- fs.mkdirSync(storeDir, { recursive: true });
127
- fs.writeFileSync(path.join(storeDir, STORE_FILE), JSON.stringify(store, null, 2) + '\n', 'utf-8');
152
+ if (signatures.length > 0) {
153
+ const store = { version: 1, signatures, updatedAt: new Date().toISOString() };
154
+ const storeDir = path.join(targetDir, STORE_DIR);
155
+ fs.mkdirSync(storeDir, { recursive: true });
156
+ fs.writeFileSync(path.join(storeDir, STORE_FILE), JSON.stringify(store, null, 2) + '\n', 'utf-8');
157
+ await emitEvent('config.signed', 'guard.sign', targetDir, 'info', 'allowed', {
158
+ fileCount: signatures.length, files: signatures.map(s => s.filePath),
159
+ });
160
+ }
161
+ let skillResults = [];
162
+ let heartbeatResults = [];
163
+ if (options.skills || options.heartbeats) {
164
+ const { signSkillFiles, signHeartbeatFiles } = await import('./guard-signing.js');
165
+ if (options.skills)
166
+ skillResults = await signSkillFiles(targetDir);
167
+ if (options.heartbeats)
168
+ heartbeatResults = await signHeartbeatFiles(targetDir);
169
+ }
128
170
  if (isJson) {
129
- process.stdout.write(JSON.stringify({ signed: signatures.length, files: signatures.map(s => s.filePath) }, null, 2) + '\n');
171
+ const result = { signed: signatures.length, files: signatures.map(s => s.filePath) };
172
+ if (skillResults.length > 0)
173
+ result.skills = skillResults.map(s => s.filePath);
174
+ if (heartbeatResults.length > 0)
175
+ result.heartbeats = heartbeatResults.map(s => s.filePath);
176
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
130
177
  }
131
178
  else {
132
- process.stdout.write((0, colors_js_1.green)(`Signed ${signatures.length} config file${signatures.length === 1 ? '' : 's'}.\n`));
133
- for (const sig of signatures) {
134
- process.stdout.write((0, colors_js_1.dim)(` ${sig.filePath} ${sig.hash.slice(0, 23)}...\n`));
179
+ if (signatures.length > 0) {
180
+ process.stdout.write((0, colors_js_1.green)(`Signed ${signatures.length} config file${signatures.length === 1 ? '' : 's'}.\n`));
181
+ for (const sig of signatures) {
182
+ process.stdout.write((0, colors_js_1.dim)(` ${sig.filePath} ${sig.hash.slice(0, 23)}...\n`));
183
+ }
184
+ }
185
+ if (skillResults.length > 0) {
186
+ process.stdout.write((0, colors_js_1.green)(`Signed ${skillResults.length} skill file${skillResults.length === 1 ? '' : 's'}.\n`));
187
+ for (const sr of skillResults) {
188
+ process.stdout.write((0, colors_js_1.dim)(` ${sr.filePath} ${sr.hash.slice(0, 23)}...\n`));
189
+ }
135
190
  }
136
- process.stdout.write((0, colors_js_1.dim)(`\nStore: ${STORE_DIR}/${STORE_FILE}\n`));
191
+ if (heartbeatResults.length > 0) {
192
+ process.stdout.write((0, colors_js_1.green)(`Signed ${heartbeatResults.length} heartbeat file${heartbeatResults.length === 1 ? '' : 's'}.\n`));
193
+ for (const hr of heartbeatResults) {
194
+ process.stdout.write((0, colors_js_1.dim)(` ${hr.filePath} ${hr.hash.slice(0, 23)}...\n`));
195
+ }
196
+ }
197
+ if (signatures.length > 0)
198
+ process.stdout.write((0, colors_js_1.dim)(`\nStore: ${STORE_DIR}/${STORE_FILE}\n`));
137
199
  }
138
200
  return 0;
139
201
  }
140
202
  // --- Verify ---
141
203
  async function guardVerify(targetDir, options) {
142
204
  const isJson = options.format === 'json';
205
+ const enforce = options.enforce ?? false;
143
206
  const store = loadStore(targetDir);
144
207
  if (!store) {
145
208
  if (isJson) {
@@ -151,54 +214,106 @@ async function guardVerify(targetDir, options) {
151
214
  return 1;
152
215
  }
153
216
  const results = [];
154
- // Check signed files
155
217
  for (const sig of store.signatures) {
156
218
  const fullPath = path.join(targetDir, sig.filePath);
157
219
  if (!fs.existsSync(fullPath)) {
158
- results.push({
159
- filePath: sig.filePath,
160
- status: 'missing',
161
- expectedHash: sig.hash,
162
- });
220
+ results.push({ filePath: sig.filePath, status: 'missing', expectedHash: sig.hash });
163
221
  continue;
164
222
  }
165
223
  const content = fs.readFileSync(fullPath);
166
224
  const currentHash = 'sha256:' + (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
167
225
  if (currentHash === sig.hash) {
168
- results.push({
169
- filePath: sig.filePath,
170
- status: 'pass',
171
- currentHash,
172
- });
226
+ results.push({ filePath: sig.filePath, status: 'pass', currentHash });
173
227
  }
174
228
  else {
175
- results.push({
176
- filePath: sig.filePath,
177
- status: 'tampered',
178
- currentHash,
179
- expectedHash: sig.hash,
180
- });
229
+ const diff = computeFileDiff(fullPath, sig, content);
230
+ results.push({ filePath: sig.filePath, status: 'tampered', currentHash, expectedHash: sig.hash, diff });
181
231
  }
182
232
  }
183
- // Check for unsigned config files
184
233
  const signedPaths = new Set(store.signatures.map(s => s.filePath));
185
- const allConfigFiles = resolveFiles(targetDir);
186
- for (const relPath of allConfigFiles) {
234
+ for (const relPath of resolveFiles(targetDir)) {
187
235
  if (!signedPaths.has(relPath)) {
188
- results.push({
189
- filePath: relPath,
190
- status: 'unsigned',
191
- });
236
+ results.push({ filePath: relPath, status: 'unsigned' });
192
237
  }
193
238
  }
194
239
  const report = buildReport('verify', targetDir, results, store.signatures.length);
240
+ const tamperedFiles = results.filter(r => r.status === 'tampered' || r.status === 'missing');
241
+ if (tamperedFiles.length > 0) {
242
+ await emitEvent('config.tampered', 'guard.verify', targetDir, enforce ? 'high' : 'medium', enforce ? 'blocked' : 'monitored', {
243
+ tamperedCount: report.tampered, missingCount: report.missing,
244
+ files: tamperedFiles.map(f => ({ path: f.filePath, status: f.status, diff: f.diff ?? null })),
245
+ });
246
+ }
247
+ else {
248
+ await emitEvent('config.verified', 'guard.verify', targetDir, 'info', 'allowed', {
249
+ passedCount: report.passed, unsignedCount: report.unsigned, totalSigned: report.totalSigned,
250
+ });
251
+ }
252
+ // Policy-aware enforcement: heartbeat disable on tamper, blockOnUnsigned
253
+ let policyViolations = 0;
254
+ try {
255
+ const { loadGuardPolicy, checkPolicyCompliance, disableHeartbeat } = await import('./guard-policy.js');
256
+ const policy = loadGuardPolicy(targetDir);
257
+ if (policy) {
258
+ const compliance = checkPolicyCompliance(targetDir, policy);
259
+ // Disable heartbeat on tamper if policy requires it
260
+ if (policy.disableHeartbeatOnTamper && (compliance.requiredTampered.length > 0 || compliance.requiredMissing.length > 0)) {
261
+ const reason = `Tamper detected: ${[...compliance.requiredTampered, ...compliance.requiredMissing].join(', ')}`;
262
+ disableHeartbeat(targetDir, reason);
263
+ await emitEvent('heartbeat.disabled', 'guard.verify', targetDir, 'high', 'blocked', { reason, tampered: compliance.requiredTampered, missing: compliance.requiredMissing });
264
+ }
265
+ // Block on unsigned required files
266
+ if (policy.blockOnUnsigned && compliance.requiredUnsigned.length > 0) {
267
+ policyViolations += compliance.requiredUnsigned.length;
268
+ for (const f of compliance.requiredUnsigned) {
269
+ // Add unsigned required files as failures if not already in results
270
+ if (!results.find(r => r.filePath === f)) {
271
+ results.push({ filePath: f, status: 'unsigned' });
272
+ }
273
+ }
274
+ await emitEvent('policy.violation', 'guard.verify', targetDir, 'medium', enforce ? 'blocked' : 'monitored', { unsignedRequired: compliance.requiredUnsigned });
275
+ }
276
+ }
277
+ }
278
+ catch {
279
+ // guard-policy module not available
280
+ }
281
+ let skillVerify = [];
282
+ let heartbeatVerify = [];
283
+ if (options.skills || options.heartbeats) {
284
+ const { verifySkillSignatures, verifyHeartbeatSignatures } = await import('./guard-signing.js');
285
+ if (options.skills)
286
+ skillVerify = await verifySkillSignatures(targetDir);
287
+ if (options.heartbeats)
288
+ heartbeatVerify = await verifyHeartbeatSignatures(targetDir);
289
+ }
195
290
  if (isJson) {
196
- process.stdout.write(JSON.stringify(report, null, 2) + '\n');
291
+ const output = { ...report };
292
+ if (skillVerify.length > 0)
293
+ output.skills = skillVerify;
294
+ if (heartbeatVerify.length > 0)
295
+ output.heartbeats = heartbeatVerify;
296
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
197
297
  }
198
298
  else {
199
- printVerifyReport(report);
299
+ printVerifyReport(report, enforce);
300
+ if (skillVerify.length > 0) {
301
+ process.stdout.write((0, colors_js_1.bold)(' Skill Signatures') + '\n');
302
+ for (const sv of skillVerify) {
303
+ process.stdout.write(` ${sv.filePath.padEnd(28)} ${sv.status === 'pass' ? (0, colors_js_1.green)('PASS') : sv.status === 'tampered' ? (0, colors_js_1.red)('TAMPERED') : (0, colors_js_1.yellow)(sv.status.toUpperCase())}\n`);
304
+ }
305
+ process.stdout.write('\n');
306
+ }
307
+ if (heartbeatVerify.length > 0) {
308
+ process.stdout.write((0, colors_js_1.bold)(' Heartbeat Signatures') + '\n');
309
+ for (const hv of heartbeatVerify) {
310
+ process.stdout.write(` ${hv.filePath.padEnd(28)} ${hv.status === 'pass' ? (0, colors_js_1.green)('PASS') : hv.status === 'expired' ? (0, colors_js_1.yellow)('EXPIRED') : hv.status === 'tampered' ? (0, colors_js_1.red)('TAMPERED') : (0, colors_js_1.yellow)(hv.status.toUpperCase())}\n`);
311
+ }
312
+ process.stdout.write('\n');
313
+ }
200
314
  }
201
- return (report.tampered > 0 || report.missing > 0) ? 1 : 0;
315
+ const sigFailed = [...skillVerify, ...heartbeatVerify].some(v => v.status !== 'pass');
316
+ return (report.tampered > 0 || report.missing > 0 || sigFailed || policyViolations > 0) ? (enforce ? EXIT_QUARANTINE : 1) : 0;
202
317
  }
203
318
  // --- Status ---
204
319
  async function guardStatus(targetDir, options) {
@@ -208,7 +323,6 @@ async function guardStatus(targetDir, options) {
208
323
  const allConfigFiles = resolveFiles(targetDir);
209
324
  const signedPaths = new Set(store?.signatures.map(s => s.filePath) ?? []);
210
325
  const unsignedCount = allConfigFiles.filter(f => !signedPaths.has(f)).length;
211
- // Quick tamper check
212
326
  let tamperedCount = 0;
213
327
  if (store) {
214
328
  for (const sig of store.signatures) {
@@ -223,21 +337,34 @@ async function guardStatus(targetDir, options) {
223
337
  tamperedCount++;
224
338
  }
225
339
  }
226
- const statusReport = {
227
- signed: signedCount,
228
- unsigned: unsignedCount,
229
- tampered: tamperedCount,
230
- lastUpdated: store?.updatedAt ?? null,
231
- };
340
+ // Skill and heartbeat counts for status
341
+ let skillCount = 0;
342
+ let heartbeatCount = 0;
343
+ if (options.skills || options.heartbeats) {
344
+ const { _internals: sigInternals } = await import('./guard-signing.js');
345
+ if (options.skills)
346
+ skillCount = sigInternals.findFiles(targetDir, sigInternals.SKILL_PATTERNS).length;
347
+ if (options.heartbeats)
348
+ heartbeatCount = sigInternals.findFiles(targetDir, sigInternals.HEARTBEAT_PATTERNS).length;
349
+ }
350
+ const statusReport = { signed: signedCount, unsigned: unsignedCount, tampered: tamperedCount, lastUpdated: store?.updatedAt ?? null };
351
+ if (skillCount > 0)
352
+ statusReport.skills = skillCount;
353
+ if (heartbeatCount > 0)
354
+ statusReport.heartbeats = heartbeatCount;
232
355
  if (isJson) {
233
356
  process.stdout.write(JSON.stringify(statusReport, null, 2) + '\n');
234
357
  }
235
358
  else {
236
359
  process.stdout.write((0, colors_js_1.bold)('ConfigGuard Status') + '\n');
237
360
  process.stdout.write((0, colors_js_1.gray)('-'.repeat(40)) + '\n');
238
- process.stdout.write(` Signed: ${(0, colors_js_1.green)(String(signedCount))}\n`);
239
- process.stdout.write(` Unsigned: ${unsignedCount > 0 ? (0, colors_js_1.yellow)(String(unsignedCount)) : (0, colors_js_1.dim)('0')}\n`);
240
- process.stdout.write(` Tampered: ${tamperedCount > 0 ? (0, colors_js_1.red)(String(tamperedCount)) : (0, colors_js_1.dim)('0')}\n`);
361
+ process.stdout.write(` Signed: ${(0, colors_js_1.green)(String(signedCount))}\n`);
362
+ process.stdout.write(` Unsigned: ${unsignedCount > 0 ? (0, colors_js_1.yellow)(String(unsignedCount)) : (0, colors_js_1.dim)('0')}\n`);
363
+ process.stdout.write(` Tampered: ${tamperedCount > 0 ? (0, colors_js_1.red)(String(tamperedCount)) : (0, colors_js_1.dim)('0')}\n`);
364
+ if (skillCount > 0)
365
+ process.stdout.write(` Skills: ${(0, colors_js_1.green)(String(skillCount))}\n`);
366
+ if (heartbeatCount > 0)
367
+ process.stdout.write(` Heartbeats: ${(0, colors_js_1.green)(String(heartbeatCount))}\n`);
241
368
  if (store?.updatedAt) {
242
369
  process.stdout.write((0, colors_js_1.dim)(` Last signed: ${store.updatedAt}\n`));
243
370
  }
@@ -245,11 +372,261 @@ async function guardStatus(targetDir, options) {
245
372
  }
246
373
  return tamperedCount > 0 ? 1 : 0;
247
374
  }
375
+ // --- Watch ---
376
+ async function guardWatch(targetDir, options) {
377
+ const isJson = options.format === 'json';
378
+ const enforce = options.enforce ?? false;
379
+ const store = loadStore(targetDir);
380
+ if (!store) {
381
+ if (isJson) {
382
+ process.stdout.write(JSON.stringify({ error: 'No signature store found. Run: opena2a guard sign' }, null, 2) + '\n');
383
+ }
384
+ else {
385
+ process.stdout.write((0, colors_js_1.yellow)('No signature store found. Run: opena2a guard sign\n'));
386
+ }
387
+ return 1;
388
+ }
389
+ if (!isJson) {
390
+ process.stdout.write((0, colors_js_1.bold)('ConfigGuard Watch') + '\n');
391
+ process.stdout.write((0, colors_js_1.dim)(`Monitoring ${store.signatures.length} signed file(s)...\n`));
392
+ process.stdout.write((0, colors_js_1.dim)('Press Ctrl+C to stop.\n\n'));
393
+ }
394
+ const watchers = [];
395
+ const debounceTimers = new Map();
396
+ for (const sig of store.signatures) {
397
+ const fullPath = path.join(targetDir, sig.filePath);
398
+ if (!fs.existsSync(fullPath))
399
+ continue;
400
+ try {
401
+ const watcher = fs.watch(fullPath, { persistent: true }, () => {
402
+ const existing = debounceTimers.get(sig.filePath);
403
+ if (existing)
404
+ clearTimeout(existing);
405
+ debounceTimers.set(sig.filePath, setTimeout(async () => {
406
+ debounceTimers.delete(sig.filePath);
407
+ await handleFileChange(targetDir, sig, isJson, enforce, options.verbose);
408
+ }, 100));
409
+ });
410
+ watchers.push(watcher);
411
+ }
412
+ catch {
413
+ if (!isJson)
414
+ process.stderr.write((0, colors_js_1.yellow)(` Cannot watch: ${sig.filePath}\n`));
415
+ }
416
+ }
417
+ if (watchers.length === 0) {
418
+ if (!isJson)
419
+ process.stdout.write((0, colors_js_1.yellow)('No files to watch.\n'));
420
+ return 1;
421
+ }
422
+ return new Promise((resolve) => {
423
+ const cleanup = () => {
424
+ for (const w of watchers) {
425
+ try {
426
+ w.close();
427
+ }
428
+ catch { /* noop */ }
429
+ }
430
+ for (const timer of debounceTimers.values())
431
+ clearTimeout(timer);
432
+ if (!isJson)
433
+ process.stdout.write((0, colors_js_1.dim)('\nWatch stopped.\n'));
434
+ resolve(0);
435
+ };
436
+ process.once('SIGINT', cleanup);
437
+ process.once('SIGTERM', cleanup);
438
+ });
439
+ }
440
+ async function handleFileChange(targetDir, sig, isJson, enforce, verbose) {
441
+ const fullPath = path.join(targetDir, sig.filePath);
442
+ const timestamp = new Date().toISOString();
443
+ if (!fs.existsSync(fullPath)) {
444
+ if (isJson) {
445
+ process.stdout.write(JSON.stringify({ time: timestamp, file: sig.filePath, status: 'missing' }) + '\n');
446
+ }
447
+ else {
448
+ process.stdout.write(`${(0, colors_js_1.dim)(timestamp.slice(11, 19))} ${(0, colors_js_1.red)('MISSING')} ${sig.filePath}\n`);
449
+ }
450
+ await emitEvent('config.tampered', 'guard.watch', sig.filePath, 'high', enforce ? 'blocked' : 'monitored', { reason: 'file_deleted' });
451
+ return;
452
+ }
453
+ const content = fs.readFileSync(fullPath);
454
+ const currentHash = 'sha256:' + (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
455
+ if (currentHash === sig.hash) {
456
+ if (verbose) {
457
+ if (isJson) {
458
+ process.stdout.write(JSON.stringify({ time: timestamp, file: sig.filePath, status: 'unchanged' }) + '\n');
459
+ }
460
+ else {
461
+ process.stdout.write(`${(0, colors_js_1.dim)(timestamp.slice(11, 19))} ${(0, colors_js_1.dim)('OK')} ${(0, colors_js_1.dim)(sig.filePath)}\n`);
462
+ }
463
+ }
464
+ return;
465
+ }
466
+ const diff = computeFileDiff(fullPath, sig, content);
467
+ if (isJson) {
468
+ process.stdout.write(JSON.stringify({ time: timestamp, file: sig.filePath, status: 'tampered', diff }) + '\n');
469
+ }
470
+ else {
471
+ process.stdout.write(`${(0, colors_js_1.dim)(timestamp.slice(11, 19))} ${(0, colors_js_1.red)('TAMPERED')} ${sig.filePath}`);
472
+ if (diff) {
473
+ const changes = [];
474
+ if (diff.added?.length)
475
+ changes.push(`+${diff.added.length} keys`);
476
+ if (diff.removed?.length)
477
+ changes.push(`-${diff.removed.length} keys`);
478
+ if (diff.modified?.length)
479
+ changes.push(`~${diff.modified.length} keys`);
480
+ if (diff.sizeChange !== 0)
481
+ changes.push(`${diff.sizeChange > 0 ? '+' : ''}${diff.sizeChange}b`);
482
+ if (changes.length > 0)
483
+ process.stdout.write((0, colors_js_1.dim)(` (${changes.join(', ')})`));
484
+ }
485
+ process.stdout.write('\n');
486
+ }
487
+ await emitEvent('config.tampered', 'guard.watch', sig.filePath, enforce ? 'high' : 'medium', enforce ? 'blocked' : 'monitored', { currentHash, expectedHash: sig.hash, diff });
488
+ }
489
+ // --- Diff ---
490
+ async function guardDiff(targetDir, options) {
491
+ const isJson = options.format === 'json';
492
+ const store = loadStore(targetDir);
493
+ if (!store) {
494
+ if (isJson) {
495
+ process.stdout.write(JSON.stringify({ error: 'No signature store found. Run: opena2a guard sign' }, null, 2) + '\n');
496
+ }
497
+ else {
498
+ process.stdout.write((0, colors_js_1.yellow)('No signature store found. Run: opena2a guard sign\n'));
499
+ }
500
+ return 1;
501
+ }
502
+ const filesToCheck = options.files ? store.signatures.filter(s => options.files.includes(s.filePath)) : store.signatures;
503
+ const diffs = [];
504
+ let hasChanges = false;
505
+ for (const sig of filesToCheck) {
506
+ const fullPath = path.join(targetDir, sig.filePath);
507
+ if (!fs.existsSync(fullPath)) {
508
+ diffs.push({ filePath: sig.filePath, status: 'missing', diff: null });
509
+ hasChanges = true;
510
+ continue;
511
+ }
512
+ const content = fs.readFileSync(fullPath);
513
+ const currentHash = 'sha256:' + (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
514
+ if (currentHash === sig.hash) {
515
+ diffs.push({ filePath: sig.filePath, status: 'unchanged', diff: null });
516
+ continue;
517
+ }
518
+ hasChanges = true;
519
+ diffs.push({ filePath: sig.filePath, status: 'changed', diff: computeFileDiff(fullPath, sig, content) });
520
+ }
521
+ if (isJson) {
522
+ process.stdout.write(JSON.stringify({ files: diffs, hasChanges }, null, 2) + '\n');
523
+ return hasChanges ? 1 : 0;
524
+ }
525
+ process.stdout.write((0, colors_js_1.bold)('ConfigGuard Diff') + '\n');
526
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
527
+ for (const entry of diffs) {
528
+ if (entry.status === 'unchanged') {
529
+ if (options.verbose)
530
+ process.stdout.write(` ${(0, colors_js_1.dim)('--')} ${(0, colors_js_1.dim)(entry.filePath)} ${(0, colors_js_1.dim)('(unchanged)')}\n`);
531
+ continue;
532
+ }
533
+ if (entry.status === 'missing') {
534
+ process.stdout.write(` ${(0, colors_js_1.red)('MISSING')} ${entry.filePath}\n`);
535
+ continue;
536
+ }
537
+ process.stdout.write(` ${(0, colors_js_1.yellow)('CHANGED')} ${entry.filePath}\n`);
538
+ if (entry.diff) {
539
+ if (entry.diff.sizeChange !== 0) {
540
+ const sign = entry.diff.sizeChange > 0 ? '+' : '';
541
+ process.stdout.write((0, colors_js_1.dim)(` Size: ${sign}${entry.diff.sizeChange} bytes\n`));
542
+ }
543
+ if (entry.diff.added?.length) {
544
+ for (const key of entry.diff.added)
545
+ process.stdout.write(` ${(0, colors_js_1.green)('+ ' + key)}\n`);
546
+ }
547
+ if (entry.diff.removed?.length) {
548
+ for (const key of entry.diff.removed)
549
+ process.stdout.write(` ${(0, colors_js_1.red)('- ' + key)}\n`);
550
+ }
551
+ if (entry.diff.modified?.length) {
552
+ for (const key of entry.diff.modified)
553
+ process.stdout.write(` ${(0, colors_js_1.cyan)('~ ' + key)}\n`);
554
+ }
555
+ }
556
+ }
557
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
558
+ return hasChanges ? 1 : 0;
559
+ }
560
+ // --- Shield Integration ---
561
+ function verifyConfigIntegrity(targetDir) {
562
+ const dir = targetDir ?? process.cwd();
563
+ const store = loadStore(dir);
564
+ if (!store || store.signatures.length === 0) {
565
+ return { filesMonitored: 0, tamperedFiles: [], signatureStatus: 'unsigned' };
566
+ }
567
+ const tampered = [];
568
+ for (const sig of store.signatures) {
569
+ const fullPath = path.join(dir, sig.filePath);
570
+ if (!fs.existsSync(fullPath)) {
571
+ tampered.push(sig.filePath);
572
+ continue;
573
+ }
574
+ const content = fs.readFileSync(fullPath);
575
+ const currentHash = 'sha256:' + (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
576
+ if (currentHash !== sig.hash)
577
+ tampered.push(sig.filePath);
578
+ }
579
+ return { filesMonitored: store.signatures.length, tamperedFiles: tampered, signatureStatus: tampered.length > 0 ? 'tampered' : 'valid' };
580
+ }
581
+ // --- Diff computation ---
582
+ function computeFileDiff(fullPath, sig, currentContent) {
583
+ const sizeChange = currentContent.length - sig.fileSize;
584
+ if (fullPath.endsWith('.json') || fullPath.endsWith('.yaml') || fullPath.endsWith('.yml')) {
585
+ try {
586
+ JSON.parse(currentContent.toString('utf-8'));
587
+ return { type: 'json', sizeChange };
588
+ }
589
+ catch { /* not JSON */ }
590
+ }
591
+ return { type: 'text', sizeChange };
592
+ }
593
+ function diffJsonKeys(original, current) {
594
+ const origKeys = new Set(Object.keys(original));
595
+ const currKeys = new Set(Object.keys(current));
596
+ const added = [];
597
+ const removed = [];
598
+ const modified = [];
599
+ for (const key of currKeys) {
600
+ if (!origKeys.has(key))
601
+ added.push(key);
602
+ else if (JSON.stringify(original[key]) !== JSON.stringify(current[key]))
603
+ modified.push(key);
604
+ }
605
+ for (const key of origKeys) {
606
+ if (!currKeys.has(key))
607
+ removed.push(key);
608
+ }
609
+ return { added, removed, modified };
610
+ }
611
+ function flattenKeys(obj, prefix = '') {
612
+ if (obj === null || typeof obj !== 'object')
613
+ return [prefix || '(root)'];
614
+ const keys = [];
615
+ for (const [key, value] of Object.entries(obj)) {
616
+ const fullKey = prefix ? `${prefix}.${key}` : key;
617
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
618
+ keys.push(...flattenKeys(value, fullKey));
619
+ }
620
+ else {
621
+ keys.push(fullKey);
622
+ }
623
+ }
624
+ return keys;
625
+ }
248
626
  // --- Helpers ---
249
627
  function resolveFiles(targetDir, customFiles) {
250
- if (customFiles && customFiles.length > 0) {
628
+ if (customFiles && customFiles.length > 0)
251
629
  return customFiles.filter(f => fs.existsSync(path.join(targetDir, f)));
252
- }
253
630
  return GUARD_FILES.filter(f => fs.existsSync(path.join(targetDir, f)));
254
631
  }
255
632
  function loadStore(targetDir) {
@@ -257,51 +634,54 @@ function loadStore(targetDir) {
257
634
  if (!fs.existsSync(storePath))
258
635
  return null;
259
636
  try {
260
- const raw = fs.readFileSync(storePath, 'utf-8');
261
- return JSON.parse(raw);
637
+ return JSON.parse(fs.readFileSync(storePath, 'utf-8'));
262
638
  }
263
639
  catch {
264
640
  return null;
265
641
  }
266
642
  }
267
643
  function buildReport(subcommand, targetDir, results, totalSigned) {
268
- return {
269
- subcommand,
270
- directory: targetDir,
271
- results,
272
- passed: results.filter(r => r.status === 'pass').length,
273
- tampered: results.filter(r => r.status === 'tampered').length,
274
- unsigned: results.filter(r => r.status === 'unsigned').length,
275
- missing: results.filter(r => r.status === 'missing').length,
276
- totalSigned,
277
- };
644
+ return { subcommand, directory: targetDir, results,
645
+ passed: results.filter(r => r.status === 'pass').length, tampered: results.filter(r => r.status === 'tampered').length,
646
+ unsigned: results.filter(r => r.status === 'unsigned').length, missing: results.filter(r => r.status === 'missing').length, totalSigned };
278
647
  }
279
- function printVerifyReport(report) {
648
+ function printVerifyReport(report, enforce) {
280
649
  process.stdout.write('\n' + (0, colors_js_1.bold)(' ConfigGuard Verification') + '\n\n');
281
650
  process.stdout.write(` ${(0, colors_js_1.dim)('File'.padEnd(28))} ${(0, colors_js_1.dim)('Status'.padEnd(12))} ${(0, colors_js_1.dim)('Hash')}\n`);
282
651
  process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(60)) + '\n');
283
652
  for (const result of report.results) {
284
- const statusLabel = result.status === 'pass' ? (0, colors_js_1.green)('PASS')
285
- : result.status === 'tampered' ? (0, colors_js_1.red)('TAMPERED')
286
- : result.status === 'unsigned' ? (0, colors_js_1.yellow)('UNSIGNED')
287
- : (0, colors_js_1.red)('MISSING');
288
- const hashDisplay = result.currentHash
289
- ? (0, colors_js_1.dim)(result.currentHash.slice(0, 23) + '...')
290
- : (0, colors_js_1.dim)('--');
653
+ const statusLabel = result.status === 'pass' ? (0, colors_js_1.green)('PASS') : result.status === 'tampered' ? (0, colors_js_1.red)('TAMPERED') : result.status === 'unsigned' ? (0, colors_js_1.yellow)('UNSIGNED') : (0, colors_js_1.red)('MISSING');
654
+ const hashDisplay = result.currentHash ? (0, colors_js_1.dim)(result.currentHash.slice(0, 23) + '...') : (0, colors_js_1.dim)('--');
291
655
  process.stdout.write(` ${result.filePath.padEnd(28)} ${statusLabel.padEnd(20)} ${hashDisplay}\n`);
292
656
  if (result.status === 'tampered' && result.expectedHash) {
293
657
  process.stdout.write(` ${' '.repeat(28)} ${(0, colors_js_1.dim)('expected: ' + result.expectedHash.slice(0, 23) + '...')}\n`);
294
658
  }
659
+ if (result.status === 'tampered' && result.diff) {
660
+ const parts = [];
661
+ if (result.diff.sizeChange !== 0)
662
+ parts.push(`${result.diff.sizeChange > 0 ? '+' : ''}${result.diff.sizeChange}b`);
663
+ if (result.diff.added?.length)
664
+ parts.push(`+${result.diff.added.length} keys`);
665
+ if (result.diff.removed?.length)
666
+ parts.push(`-${result.diff.removed.length} keys`);
667
+ if (result.diff.modified?.length)
668
+ parts.push(`~${result.diff.modified.length} keys`);
669
+ if (parts.length > 0)
670
+ process.stdout.write(` ${' '.repeat(28)} ${(0, colors_js_1.dim)('diff: ' + parts.join(', '))}\n`);
671
+ }
295
672
  }
296
673
  process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(60)) + '\n');
297
674
  process.stdout.write(` ${(0, colors_js_1.dim)('Result:')} ${(0, colors_js_1.green)(String(report.passed))} passed, `);
298
675
  process.stdout.write(`${report.tampered > 0 ? (0, colors_js_1.red)(String(report.tampered)) : '0'} tampered, `);
299
- process.stdout.write(`${report.unsigned > 0 ? (0, colors_js_1.yellow)(String(report.unsigned)) : '0'} unsigned\n\n`);
676
+ process.stdout.write(`${report.unsigned > 0 ? (0, colors_js_1.yellow)(String(report.unsigned)) : '0'} unsigned`);
677
+ if (enforce && (report.tampered > 0 || report.missing > 0)) {
678
+ process.stdout.write(` ${(0, colors_js_1.red)('[QUARANTINE]')}`);
679
+ }
680
+ process.stdout.write('\n\n');
300
681
  }
301
682
  // --- Testable internals ---
302
683
  exports._internals = {
303
- resolveFiles,
304
- loadStore,
305
- GUARD_FILES,
684
+ resolveFiles, loadStore, computeFileDiff, diffJsonKeys, flattenKeys, emitEvent, verifyConfigIntegrity,
685
+ GUARD_FILES, STORE_DIR, STORE_FILE, EXIT_QUARANTINE,
306
686
  };
307
687
  //# sourceMappingURL=guard.js.map