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.
- package/README.md +225 -1
- package/dist/commands/guard-hooks.d.ts +27 -0
- package/dist/commands/guard-hooks.d.ts.map +1 -0
- package/dist/commands/guard-hooks.js +207 -0
- package/dist/commands/guard-hooks.js.map +1 -0
- package/dist/commands/guard-policy.d.ts +54 -0
- package/dist/commands/guard-policy.d.ts.map +1 -0
- package/dist/commands/guard-policy.js +251 -0
- package/dist/commands/guard-policy.js.map +1 -0
- package/dist/commands/guard-signing.d.ts +52 -0
- package/dist/commands/guard-signing.d.ts.map +1 -0
- package/dist/commands/guard-signing.js +185 -0
- package/dist/commands/guard-signing.js.map +1 -0
- package/dist/commands/guard-snapshots.d.ts +54 -0
- package/dist/commands/guard-snapshots.d.ts.map +1 -0
- package/dist/commands/guard-snapshots.js +346 -0
- package/dist/commands/guard-snapshots.js.map +1 -0
- package/dist/commands/guard.d.ts +60 -4
- package/dist/commands/guard.d.ts.map +1 -1
- package/dist/commands/guard.js +475 -95
- package/dist/commands/guard.js.map +1 -1
- package/dist/commands/init.js +3 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/shield.d.ts +3 -0
- package/dist/commands/shield.d.ts.map +1 -1
- package/dist/commands/shield.js +458 -30
- package/dist/commands/shield.js.map +1 -1
- package/dist/index.js +15 -6
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1 -0
- package/dist/router.js.map +1 -1
- package/dist/shield/arp-bridge.d.ts +62 -0
- package/dist/shield/arp-bridge.d.ts.map +1 -0
- package/dist/shield/arp-bridge.js +198 -0
- package/dist/shield/arp-bridge.js.map +1 -0
- package/dist/shield/baselines.d.ts +58 -0
- package/dist/shield/baselines.d.ts.map +1 -0
- package/dist/shield/baselines.js +371 -0
- package/dist/shield/baselines.js.map +1 -0
- package/dist/shield/findings.d.ts +52 -0
- package/dist/shield/findings.d.ts.map +1 -0
- package/dist/shield/findings.js +336 -0
- package/dist/shield/findings.js.map +1 -0
- package/dist/shield/integrity.d.ts.map +1 -1
- package/dist/shield/integrity.js +6 -2
- package/dist/shield/integrity.js.map +1 -1
- package/dist/shield/report-html.d.ts +29 -0
- package/dist/shield/report-html.d.ts.map +1 -0
- package/dist/shield/report-html.js +596 -0
- package/dist/shield/report-html.js.map +1 -0
- package/dist/shield/sarif.d.ts +65 -0
- package/dist/shield/sarif.d.ts.map +1 -0
- package/dist/shield/sarif.js +108 -0
- package/dist/shield/sarif.js.map +1 -0
- package/dist/shield/status.d.ts.map +1 -1
- package/dist/shield/status.js +6 -6
- package/dist/shield/status.js.map +1 -1
- package/dist/shield/types.d.ts +19 -1
- package/dist/shield/types.d.ts.map +1 -1
- package/dist/shield/types.js +2 -1
- package/dist/shield/types.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/guard.js
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
* opena2a guard -- ConfigGuard: config file integrity signing and verification.
|
|
4
4
|
*
|
|
5
5
|
* Subcommands:
|
|
6
|
-
* - sign:
|
|
7
|
-
* - verify:
|
|
8
|
-
* - status:
|
|
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
|
-
|
|
75
|
-
case '
|
|
76
|
-
|
|
77
|
-
case '
|
|
78
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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:
|
|
239
|
-
process.stdout.write(` Unsigned:
|
|
240
|
-
process.stdout.write(` Tampered:
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|