@panguard-ai/panguard 0.3.4 → 0.3.6
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/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +106 -49
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +96 -53
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +32 -6
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/skills.d.ts +7 -0
- package/dist/cli/commands/skills.d.ts.map +1 -0
- package/dist/cli/commands/skills.js +144 -0
- package/dist/cli/commands/skills.js.map +1 -0
- package/dist/cli/commands/threat.d.ts.map +1 -1
- package/dist/cli/commands/threat.js +12 -4
- package/dist/cli/commands/threat.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/package.json +23 -22
- package/LICENSE +0 -21
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/audit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/audit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBpC,wBAAgB,YAAY,IAAI,OAAO,CAsKtC"}
|
|
@@ -3,8 +3,22 @@
|
|
|
3
3
|
* panguard audit - 技能安全審計命令
|
|
4
4
|
*/
|
|
5
5
|
import { Command } from 'commander';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
6
8
|
import path from 'node:path';
|
|
7
9
|
import { c, banner, divider, box, symbols, setLogLevel } from '@panguard-ai/core';
|
|
10
|
+
/** Default Threat Cloud endpoint */
|
|
11
|
+
const DEFAULT_TC_ENDPOINT = 'https://tc.panguard.ai';
|
|
12
|
+
/**
|
|
13
|
+
* Compute a SHA-256 hash of a skill's SKILL.md content for anonymized tracking.
|
|
14
|
+
*/
|
|
15
|
+
function computeSkillHash(skillDir) {
|
|
16
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
17
|
+
if (!existsSync(skillMdPath))
|
|
18
|
+
return createHash('sha256').update(skillDir).digest('hex');
|
|
19
|
+
const content = readFileSync(skillMdPath, 'utf-8');
|
|
20
|
+
return createHash('sha256').update(content).digest('hex');
|
|
21
|
+
}
|
|
8
22
|
export function auditCommand() {
|
|
9
23
|
const cmd = new Command('audit').description('Audit security of OpenClaw skills / 審計 OpenClaw 技能的安全性');
|
|
10
24
|
cmd
|
|
@@ -13,6 +27,8 @@ export function auditCommand() {
|
|
|
13
27
|
.argument('<path>', 'Path to skill directory containing SKILL.md / 包含 SKILL.md 的技能目錄路徑')
|
|
14
28
|
.option('--json', 'Output as JSON / 以 JSON 格式輸出', false)
|
|
15
29
|
.option('--verbose', 'Verbose output / 詳細輸出', false)
|
|
30
|
+
.option('--no-cloud', 'Skip Threat Cloud submission / 不回報至 Threat Cloud')
|
|
31
|
+
.option('--tc-endpoint <url>', 'Threat Cloud endpoint', DEFAULT_TC_ENDPOINT)
|
|
16
32
|
.action(async (skillPath, options) => {
|
|
17
33
|
if (options.verbose)
|
|
18
34
|
setLogLevel('debug');
|
|
@@ -26,59 +42,100 @@ export function auditCommand() {
|
|
|
26
42
|
const report = await auditSkill(resolvedPath);
|
|
27
43
|
if (options.json) {
|
|
28
44
|
console.log(JSON.stringify(report, null, 2));
|
|
29
|
-
return;
|
|
30
45
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for (const check of report.checks) {
|
|
49
|
-
const icon = check.status === 'pass'
|
|
50
|
-
? c.green(symbols.pass)
|
|
51
|
-
: check.status === 'fail'
|
|
52
|
-
? c.red(symbols.fail)
|
|
53
|
-
: check.status === 'warn'
|
|
54
|
-
? c.yellow(symbols.warn)
|
|
55
|
-
: c.blue(symbols.info);
|
|
56
|
-
const statusLabel = check.status === 'pass'
|
|
57
|
-
? 'PASS'
|
|
58
|
-
: check.status === 'fail'
|
|
59
|
-
? 'FAIL'
|
|
60
|
-
: check.status === 'warn'
|
|
61
|
-
? 'WARN'
|
|
62
|
-
: 'INFO';
|
|
63
|
-
console.log(` ${icon} [${statusLabel}] ${check.label}`);
|
|
64
|
-
}
|
|
65
|
-
console.log();
|
|
66
|
-
divider();
|
|
67
|
-
if (report.findings.length > 0 && options.verbose) {
|
|
46
|
+
else {
|
|
47
|
+
// Pretty output
|
|
48
|
+
const levelColors = {
|
|
49
|
+
LOW: c.green,
|
|
50
|
+
MEDIUM: c.yellow,
|
|
51
|
+
HIGH: c.red,
|
|
52
|
+
CRITICAL: (s) => c.bold(c.red(s)),
|
|
53
|
+
};
|
|
54
|
+
const colorFn = levelColors[report.riskLevel] ?? c.dim;
|
|
55
|
+
console.log(box([
|
|
56
|
+
`${c.bold('Panguard Skill Audit Report')}`,
|
|
57
|
+
'',
|
|
58
|
+
`Skill: ${report.manifest?.name ?? 'Unknown'}${report.manifest?.metadata?.version ? ` v${report.manifest.metadata.version}` : ''}`,
|
|
59
|
+
`Author: ${report.manifest?.metadata?.author ?? 'Unknown'}`,
|
|
60
|
+
`Risk Score: ${colorFn(`${report.riskScore}/100 (${report.riskLevel})`)}`,
|
|
61
|
+
`Duration: ${report.durationMs}ms`,
|
|
62
|
+
].join('\n')));
|
|
68
63
|
console.log();
|
|
69
|
-
|
|
64
|
+
for (const check of report.checks) {
|
|
65
|
+
const icon = check.status === 'pass'
|
|
66
|
+
? c.green(symbols.pass)
|
|
67
|
+
: check.status === 'fail'
|
|
68
|
+
? c.red(symbols.fail)
|
|
69
|
+
: check.status === 'warn'
|
|
70
|
+
? c.yellow(symbols.warn)
|
|
71
|
+
: c.blue(symbols.info);
|
|
72
|
+
const statusLabel = check.status === 'pass'
|
|
73
|
+
? 'PASS'
|
|
74
|
+
: check.status === 'fail'
|
|
75
|
+
? 'FAIL'
|
|
76
|
+
: check.status === 'warn'
|
|
77
|
+
? 'WARN'
|
|
78
|
+
: 'INFO';
|
|
79
|
+
console.log(` ${icon} [${statusLabel}] ${check.label}`);
|
|
80
|
+
}
|
|
70
81
|
console.log();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
? c.red
|
|
74
|
-
: finding.severity === 'high'
|
|
75
|
-
? c.yellow
|
|
76
|
-
: c.dim;
|
|
77
|
-
console.log(` ${sevColor(`[${finding.severity.toUpperCase()}]`)} ${finding.title}`);
|
|
78
|
-
console.log(` ${c.dim(finding.description)}`);
|
|
79
|
-
if (finding.location)
|
|
80
|
-
console.log(` ${c.dim(`at ${finding.location}`)}`);
|
|
82
|
+
divider();
|
|
83
|
+
if (report.findings.length > 0 && options.verbose) {
|
|
81
84
|
console.log();
|
|
85
|
+
console.log(c.bold(' Detailed Findings:'));
|
|
86
|
+
console.log();
|
|
87
|
+
for (const finding of report.findings) {
|
|
88
|
+
const sevColor = finding.severity === 'critical'
|
|
89
|
+
? c.red
|
|
90
|
+
: finding.severity === 'high'
|
|
91
|
+
? c.yellow
|
|
92
|
+
: c.dim;
|
|
93
|
+
console.log(` ${sevColor(`[${finding.severity.toUpperCase()}]`)} ${finding.title}`);
|
|
94
|
+
console.log(` ${c.dim(finding.description)}`);
|
|
95
|
+
if (finding.location)
|
|
96
|
+
console.log(` ${c.dim(`at ${finding.location}`)}`);
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ── Report to Threat Cloud (flywheel) ──
|
|
102
|
+
if (options.cloud && report.riskScore > 0) {
|
|
103
|
+
const skillHash = computeSkillHash(resolvedPath);
|
|
104
|
+
const skillName = report.manifest?.name ?? path.basename(resolvedPath);
|
|
105
|
+
const submission = {
|
|
106
|
+
skillHash,
|
|
107
|
+
skillName,
|
|
108
|
+
riskScore: report.riskScore,
|
|
109
|
+
riskLevel: report.riskLevel,
|
|
110
|
+
findingSummaries: report.findings.slice(0, 10).map((f) => ({
|
|
111
|
+
id: f.id,
|
|
112
|
+
category: f.category,
|
|
113
|
+
severity: f.severity,
|
|
114
|
+
title: f.title,
|
|
115
|
+
})),
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
const { ThreatCloudClient } = await import('@panguard-ai/panguard-guard');
|
|
119
|
+
const dataDir = path.join(process.env['HOME'] ?? process.env['USERPROFILE'] ?? '.', '.panguard-guard');
|
|
120
|
+
const tc = new ThreatCloudClient(options.tcEndpoint, dataDir);
|
|
121
|
+
const submitted = await tc.submitSkillThreat(submission);
|
|
122
|
+
if (!options.json) {
|
|
123
|
+
if (submitted) {
|
|
124
|
+
console.log();
|
|
125
|
+
console.log(` ${c.green(symbols.pass)} ${c.dim('Threat intelligence shared with Threat Cloud')}`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log();
|
|
129
|
+
console.log(` ${c.dim(`${symbols.info} Threat Cloud offline — results saved locally`)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Threat Cloud submission is best-effort — never block the audit
|
|
135
|
+
if (!options.json) {
|
|
136
|
+
console.log();
|
|
137
|
+
console.log(` ${c.dim(`${symbols.info} Threat Cloud unavailable — results saved locally`)}`);
|
|
138
|
+
}
|
|
82
139
|
}
|
|
83
140
|
}
|
|
84
141
|
// Exit code
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/cli/commands/audit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAElF,MAAM,UAAU,YAAY;IAC1B,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,WAAW,CAC1C,wDAAwD,CACzD,CAAC;IAEF,GAAG;SACA,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CAAC,sEAAsE,CAAC;SACnF,QAAQ,CACP,QAAQ,EACR,mEAAmE,CACpE;SACA,MAAM,CAAC,QAAQ,EAAE,8BAA8B,EAAE,KAAK,CAAC;SACvD,MAAM,CAAC,WAAW,EAAE,uBAAuB,EAAE,KAAK,CAAC;SACnD,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/cli/commands/audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmCpC,wBAAgB,YAAY,IAAI,OAAO,CAiXtC"}
|
|
@@ -99,8 +99,13 @@ export function serveCommand() {
|
|
|
99
99
|
}
|
|
100
100
|
console.log('');
|
|
101
101
|
}
|
|
102
|
-
catch {
|
|
103
|
-
|
|
102
|
+
catch (err) {
|
|
103
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
104
|
+
console.error(` [ERROR] Threat Cloud initialization failed: ${msg}`);
|
|
105
|
+
if (err instanceof Error && err.stack) {
|
|
106
|
+
console.error(` ${err.stack}`);
|
|
107
|
+
}
|
|
108
|
+
console.log(` ${c.dim('Threat Cloud API routes disabled due to error above')}`);
|
|
104
109
|
console.log('');
|
|
105
110
|
}
|
|
106
111
|
// Initialize LLM Reviewer for ATR proposals (optional — needs ANTHROPIC_API_KEY)
|
|
@@ -197,14 +202,6 @@ export function serveCommand() {
|
|
|
197
202
|
privateKey: process.env['GOOGLE_SHEETS_PRIVATE_KEY'] ?? '',
|
|
198
203
|
}
|
|
199
204
|
: undefined;
|
|
200
|
-
const lemonsqueezy = process.env['LEMON_SQUEEZY_API_KEY']
|
|
201
|
-
? {
|
|
202
|
-
apiKey: process.env['LEMON_SQUEEZY_API_KEY'],
|
|
203
|
-
storeId: process.env['LEMON_SQUEEZY_STORE_ID'] ?? '',
|
|
204
|
-
webhookSecret: process.env['LEMON_SQUEEZY_WEBHOOK_SECRET'] ?? '',
|
|
205
|
-
variantTierMap: JSON.parse(process.env['LEMON_SQUEEZY_VARIANT_MAP'] ?? '{}'),
|
|
206
|
-
}
|
|
207
|
-
: undefined;
|
|
208
205
|
const baseUrl = process.env['PANGUARD_BASE_URL'] ?? `http://${host}:${port}`;
|
|
209
206
|
const authConfig = {
|
|
210
207
|
db,
|
|
@@ -212,7 +209,6 @@ export function serveCommand() {
|
|
|
212
209
|
baseUrl,
|
|
213
210
|
google,
|
|
214
211
|
sheets,
|
|
215
|
-
lemonsqueezy,
|
|
216
212
|
};
|
|
217
213
|
const handlers = createAuthHandlers(authConfig);
|
|
218
214
|
// Initialize Manager proxy for agent/event admin API routes
|
|
@@ -242,9 +238,6 @@ export function serveCommand() {
|
|
|
242
238
|
console.log(` Routes:`);
|
|
243
239
|
console.log(` ${c.dim('/api/auth/*')} Auth API`);
|
|
244
240
|
console.log(` ${c.dim('/api/admin/*')} Admin API`);
|
|
245
|
-
if (lemonsqueezy) {
|
|
246
|
-
console.log(` ${c.dim('/api/billing/*')} Billing API (Lemon Squeezy)`);
|
|
247
|
-
}
|
|
248
241
|
console.log(` ${c.dim('/api/usage/*')} Usage & Quota API`);
|
|
249
242
|
if (adminDir) {
|
|
250
243
|
console.log(` ${c.dim('/admin')} Admin Dashboard`);
|
|
@@ -265,7 +258,6 @@ export function serveCommand() {
|
|
|
265
258
|
console.log(` Services:`);
|
|
266
259
|
console.log(` Email: ${emailConfig ? ('apiKey' in emailConfig ? c.safe('Resend API') : c.safe('SMTP')) : c.caution('Not configured')}`);
|
|
267
260
|
console.log(` OAuth: ${google ? c.safe('Google') : c.dim('Not configured')}`);
|
|
268
|
-
console.log(` Billing: ${lemonsqueezy ? c.safe('Lemon Squeezy') : c.dim('Not configured')}`);
|
|
269
261
|
console.log(` Sheets: ${sheets ? c.safe('Google Sheets') : c.dim('Not configured')}`);
|
|
270
262
|
console.log(` Manager: ${c.safe(`port ${managerPort}`)}${process.env['MANAGER_AUTH_TOKEN'] ? '' : c.dim(' (no auth)')}`);
|
|
271
263
|
console.log('');
|
|
@@ -399,7 +391,6 @@ async function handleRequest(req, res, handlers, _db, adminDir, managerProxy, th
|
|
|
399
391
|
const services = {
|
|
400
392
|
email: !!(process.env['RESEND_API_KEY'] || process.env['SMTP_HOST']),
|
|
401
393
|
oauth: !!process.env['GOOGLE_CLIENT_ID'],
|
|
402
|
-
billing: !!process.env['LEMON_SQUEEZY_API_KEY'],
|
|
403
394
|
errorTracking: !!process.env['SENTRY_DSN'],
|
|
404
395
|
threatCloud: !!threatDb,
|
|
405
396
|
tcApiKey: !!process.env['TC_API_KEY'],
|
|
@@ -410,7 +401,9 @@ async function handleRequest(req, res, handlers, _db, adminDir, managerProxy, th
|
|
|
410
401
|
const s = threatDb.getStats();
|
|
411
402
|
threatStats = { rules: s.totalRules, threats: s.totalThreats };
|
|
412
403
|
}
|
|
413
|
-
catch {
|
|
404
|
+
catch {
|
|
405
|
+
/* ignore */
|
|
406
|
+
}
|
|
414
407
|
}
|
|
415
408
|
sendJson(res, 200, {
|
|
416
409
|
ok: true,
|
|
@@ -420,7 +413,10 @@ async function handleRequest(req, res, handlers, _db, adminDir, managerProxy, th
|
|
|
420
413
|
uptime: Math.round(process.uptime()),
|
|
421
414
|
db: 'connected',
|
|
422
415
|
threatStats,
|
|
423
|
-
memory: {
|
|
416
|
+
memory: {
|
|
417
|
+
rss: Math.round(mem.rss / 1024 / 1024),
|
|
418
|
+
heapUsed: Math.round(mem.heapUsed / 1024 / 1024),
|
|
419
|
+
},
|
|
424
420
|
services,
|
|
425
421
|
},
|
|
426
422
|
});
|
|
@@ -429,7 +425,20 @@ async function handleRequest(req, res, handlers, _db, adminDir, managerProxy, th
|
|
|
429
425
|
// ── Threat Cloud API Routes ────────────────────────────────────
|
|
430
426
|
// Security: rate limiting, auth, input validation
|
|
431
427
|
// Rate limit for Threat Cloud endpoints (per-IP, shared state)
|
|
432
|
-
if (threatDb &&
|
|
428
|
+
if (threatDb &&
|
|
429
|
+
pathname.startsWith('/api/') &&
|
|
430
|
+
[
|
|
431
|
+
'/api/threats',
|
|
432
|
+
'/api/rules',
|
|
433
|
+
'/api/stats',
|
|
434
|
+
'/api/atr-proposals',
|
|
435
|
+
'/api/atr-feedback',
|
|
436
|
+
'/api/skill-threats',
|
|
437
|
+
'/api/atr-rules',
|
|
438
|
+
'/api/yara-rules',
|
|
439
|
+
'/api/feeds/ip-blocklist',
|
|
440
|
+
'/api/feeds/domain-blocklist',
|
|
441
|
+
].some((p) => pathname === p)) {
|
|
433
442
|
const clientIP = req.socket.remoteAddress ?? 'unknown';
|
|
434
443
|
if (!checkTCRateLimit(clientIP)) {
|
|
435
444
|
sendJson(res, 429, { ok: false, error: 'Rate limit exceeded. Try again later.' });
|
|
@@ -455,7 +464,12 @@ async function handleRequest(req, res, handlers, _db, adminDir, managerProxy, th
|
|
|
455
464
|
sendJson(res, 400, { ok: false, error: 'Invalid JSON body' });
|
|
456
465
|
return;
|
|
457
466
|
}
|
|
458
|
-
if (!data['attackSourceIP'] ||
|
|
467
|
+
if (!data['attackSourceIP'] ||
|
|
468
|
+
!data['attackType'] ||
|
|
469
|
+
!data['mitreTechnique'] ||
|
|
470
|
+
!data['sigmaRuleMatched'] ||
|
|
471
|
+
!data['timestamp'] ||
|
|
472
|
+
!data['region']) {
|
|
459
473
|
sendJson(res, 400, { ok: false, error: 'Missing required fields' });
|
|
460
474
|
return;
|
|
461
475
|
}
|
|
@@ -511,7 +525,10 @@ async function handleRequest(req, res, handlers, _db, adminDir, managerProxy, th
|
|
|
511
525
|
return;
|
|
512
526
|
}
|
|
513
527
|
if (!rule['ruleId'] || !rule['ruleContent'] || !rule['source']) {
|
|
514
|
-
sendJson(res, 400, {
|
|
528
|
+
sendJson(res, 400, {
|
|
529
|
+
ok: false,
|
|
530
|
+
error: 'Missing required fields: ruleId, ruleContent, source',
|
|
531
|
+
});
|
|
515
532
|
return;
|
|
516
533
|
}
|
|
517
534
|
// Field-level size limits
|
|
@@ -557,30 +574,46 @@ async function handleRequest(req, res, handlers, _db, adminDir, managerProxy, th
|
|
|
557
574
|
sendJson(res, 400, { ok: false, error: 'Invalid JSON body' });
|
|
558
575
|
return;
|
|
559
576
|
}
|
|
560
|
-
if (!proposal['patternHash'] ||
|
|
577
|
+
if (!proposal['patternHash'] ||
|
|
578
|
+
!proposal['ruleContent'] ||
|
|
579
|
+
!proposal['llmProvider'] ||
|
|
580
|
+
!proposal['llmModel'] ||
|
|
581
|
+
!proposal['selfReviewVerdict']) {
|
|
561
582
|
sendJson(res, 400, { ok: false, error: 'Missing required fields' });
|
|
562
583
|
return;
|
|
563
584
|
}
|
|
564
585
|
// Validate and sanitize client ID
|
|
565
586
|
const rawClientId = req.headers['x-panguard-client-id'];
|
|
566
|
-
const clientId = typeof rawClientId === 'string' && /^[a-zA-Z0-9_-]{1,64}$/.test(rawClientId)
|
|
587
|
+
const clientId = typeof rawClientId === 'string' && /^[a-zA-Z0-9_-]{1,64}$/.test(rawClientId)
|
|
588
|
+
? rawClientId
|
|
589
|
+
: null;
|
|
567
590
|
proposal['clientId'] = clientId;
|
|
568
591
|
// Check if this pattern already has a proposal - if so, increment confirmation
|
|
569
592
|
const pHash = String(proposal['patternHash']);
|
|
570
|
-
const existing = threatDb
|
|
593
|
+
const existing = threatDb
|
|
594
|
+
.getATRProposals()
|
|
595
|
+
.find((p) => p['pattern_hash'] === pHash);
|
|
571
596
|
if (existing) {
|
|
572
597
|
threatDb.confirmATRProposal(pHash);
|
|
573
|
-
sendJson(res, 200, {
|
|
598
|
+
sendJson(res, 200, {
|
|
599
|
+
ok: true,
|
|
600
|
+
data: { message: 'Confirmation recorded', patternHash: pHash },
|
|
601
|
+
});
|
|
574
602
|
}
|
|
575
603
|
else {
|
|
576
604
|
threatDb.insertATRProposal(proposal);
|
|
577
605
|
// Fire-and-forget LLM review on first submission
|
|
578
606
|
if (llmReviewer?.isAvailable()) {
|
|
579
|
-
void llmReviewer
|
|
607
|
+
void llmReviewer
|
|
608
|
+
.reviewProposal(pHash, String(proposal['ruleContent']))
|
|
609
|
+
.catch((err) => {
|
|
580
610
|
console.error(`LLM review error for ${pHash}:`, err);
|
|
581
611
|
});
|
|
582
612
|
}
|
|
583
|
-
sendJson(res, 201, {
|
|
613
|
+
sendJson(res, 201, {
|
|
614
|
+
ok: true,
|
|
615
|
+
data: { message: 'Proposal submitted', patternHash: pHash },
|
|
616
|
+
});
|
|
584
617
|
}
|
|
585
618
|
return;
|
|
586
619
|
}
|
|
@@ -618,7 +651,10 @@ async function handleRequest(req, res, handlers, _db, adminDir, managerProxy, th
|
|
|
618
651
|
return;
|
|
619
652
|
}
|
|
620
653
|
if (!feedback['ruleId'] || typeof feedback['isTruePositive'] !== 'boolean') {
|
|
621
|
-
sendJson(res, 400, {
|
|
654
|
+
sendJson(res, 400, {
|
|
655
|
+
ok: false,
|
|
656
|
+
error: 'Missing or invalid fields: ruleId (string), isTruePositive (boolean)',
|
|
657
|
+
});
|
|
622
658
|
return;
|
|
623
659
|
}
|
|
624
660
|
const rawCid = req.headers['x-panguard-client-id'];
|
|
@@ -652,16 +688,23 @@ async function handleRequest(req, res, handlers, _db, adminDir, managerProxy, th
|
|
|
652
688
|
return;
|
|
653
689
|
}
|
|
654
690
|
const riskScore = submission['riskScore'];
|
|
655
|
-
if (typeof riskScore !== 'number' ||
|
|
691
|
+
if (typeof riskScore !== 'number' ||
|
|
692
|
+
!isFinite(riskScore) ||
|
|
693
|
+
riskScore < 0 ||
|
|
694
|
+
riskScore > 100) {
|
|
656
695
|
sendJson(res, 400, { ok: false, error: 'riskScore must be a number between 0 and 100' });
|
|
657
696
|
return;
|
|
658
697
|
}
|
|
659
698
|
if (!VALID_RISK_LEVELS.has(String(submission['riskLevel']))) {
|
|
660
|
-
sendJson(res, 400, {
|
|
699
|
+
sendJson(res, 400, {
|
|
700
|
+
ok: false,
|
|
701
|
+
error: 'riskLevel must be one of: LOW, MEDIUM, HIGH, CRITICAL',
|
|
702
|
+
});
|
|
661
703
|
return;
|
|
662
704
|
}
|
|
663
705
|
const rawCid2 = req.headers['x-panguard-client-id'];
|
|
664
|
-
submission['clientId'] =
|
|
706
|
+
submission['clientId'] =
|
|
707
|
+
typeof rawCid2 === 'string' && /^[a-zA-Z0-9_-]{1,64}$/.test(rawCid2) ? rawCid2 : null;
|
|
665
708
|
threatDb.insertSkillThreat(submission);
|
|
666
709
|
sendJson(res, 201, { ok: true, data: { message: 'Skill threat recorded' } });
|
|
667
710
|
return;
|
|
@@ -866,23 +909,6 @@ async function handleRequest(req, res, handlers, _db, adminDir, managerProxy, th
|
|
|
866
909
|
handlers.handleWaitlistList(req, res);
|
|
867
910
|
return;
|
|
868
911
|
}
|
|
869
|
-
// Billing API routes (Lemon Squeezy)
|
|
870
|
-
if (pathname === '/api/billing/webhook') {
|
|
871
|
-
await handlers.handleBillingWebhook(req, res);
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
if (pathname === '/api/billing/checkout') {
|
|
875
|
-
await handlers.handleBillingCheckout(req, res);
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
if (pathname === '/api/billing/portal') {
|
|
879
|
-
await handlers.handleBillingPortal(req, res);
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
if (pathname === '/api/billing/status') {
|
|
883
|
-
handlers.handleBillingStatus(req, res);
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
912
|
// Usage / Quota API routes
|
|
887
913
|
if (pathname === '/api/usage') {
|
|
888
914
|
handlers.handleUsageSummary(req, res);
|
|
@@ -1229,6 +1255,7 @@ function sendJson(res, status, data) {
|
|
|
1229
1255
|
// ── Threat Cloud Security Helpers ──────────────────────────────
|
|
1230
1256
|
/** Timing-safe string comparison to prevent side-channel attacks */
|
|
1231
1257
|
function timingSafeCompare(a, b) {
|
|
1258
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1232
1259
|
const { timingSafeEqual } = require('node:crypto');
|
|
1233
1260
|
const ab = Buffer.from(a);
|
|
1234
1261
|
const bb = Buffer.from(b);
|
|
@@ -1248,7 +1275,10 @@ function requireTCWriteAuth(req, res) {
|
|
|
1248
1275
|
const tcApiKey = process.env['TC_API_KEY'];
|
|
1249
1276
|
if (!tcApiKey) {
|
|
1250
1277
|
if (process.env['NODE_ENV'] === 'production') {
|
|
1251
|
-
sendJson(res, 503, {
|
|
1278
|
+
sendJson(res, 503, {
|
|
1279
|
+
ok: false,
|
|
1280
|
+
error: 'Threat Cloud write API not configured (TC_API_KEY missing)',
|
|
1281
|
+
});
|
|
1252
1282
|
return false;
|
|
1253
1283
|
}
|
|
1254
1284
|
return true; // dev passthrough
|
|
@@ -1342,8 +1372,10 @@ async function seedRulesFromBundled(threatDb) {
|
|
|
1342
1372
|
});
|
|
1343
1373
|
if (!configDir) {
|
|
1344
1374
|
console.log(` ${c.dim(' No config/ directory found — skipping rule seeding')}`);
|
|
1375
|
+
console.log(` ${c.dim(` Searched: ${configDirs.join(', ')}`)}`);
|
|
1345
1376
|
return 0;
|
|
1346
1377
|
}
|
|
1378
|
+
console.log(` ${c.dim(` Using config directory: ${configDir}`)}`);
|
|
1347
1379
|
/** Recursively collect files matching extensions */
|
|
1348
1380
|
function collectFiles(dir, extensions) {
|
|
1349
1381
|
const results = [];
|
|
@@ -1358,7 +1390,9 @@ async function seedRulesFromBundled(threatDb) {
|
|
|
1358
1390
|
}
|
|
1359
1391
|
}
|
|
1360
1392
|
}
|
|
1361
|
-
catch {
|
|
1393
|
+
catch (err) {
|
|
1394
|
+
console.error(` [WARN] Cannot read directory ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1395
|
+
}
|
|
1362
1396
|
return results;
|
|
1363
1397
|
}
|
|
1364
1398
|
// 1. Sigma rules (.yml, .yaml)
|
|
@@ -1371,8 +1405,11 @@ async function seedRulesFromBundled(threatDb) {
|
|
|
1371
1405
|
threatDb.upsertRule({ ruleId, ruleContent: content, publishedAt: now, source: 'sigma' });
|
|
1372
1406
|
seeded++;
|
|
1373
1407
|
}
|
|
1408
|
+
console.log(` ${c.dim(` Sigma: ${sigmaFiles.length} files processed`)}`);
|
|
1409
|
+
}
|
|
1410
|
+
catch (err) {
|
|
1411
|
+
console.error(` [WARN] Sigma rule seeding failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1374
1412
|
}
|
|
1375
|
-
catch { /* no sigma rules */ }
|
|
1376
1413
|
// 2. YARA rules (.yar, .yara)
|
|
1377
1414
|
const yaraDir = joinPath(configDir, 'yara-rules');
|
|
1378
1415
|
try {
|
|
@@ -1396,8 +1433,11 @@ async function seedRulesFromBundled(threatDb) {
|
|
|
1396
1433
|
seeded++;
|
|
1397
1434
|
}
|
|
1398
1435
|
}
|
|
1436
|
+
console.log(` ${c.dim(` YARA: ${yaraFiles.length} files processed`)}`);
|
|
1437
|
+
}
|
|
1438
|
+
catch (err) {
|
|
1439
|
+
console.error(` [WARN] YARA rule seeding failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1399
1440
|
}
|
|
1400
|
-
catch { /* no yara rules */ }
|
|
1401
1441
|
// 3. ATR rules (.yaml, .yml) from atr package
|
|
1402
1442
|
const atrDirs = [
|
|
1403
1443
|
joinPath(process.cwd(), 'node_modules', 'agent-threat-rules', 'rules'),
|
|
@@ -1420,8 +1460,11 @@ async function seedRulesFromBundled(threatDb) {
|
|
|
1420
1460
|
threatDb.upsertRule({ ruleId, ruleContent: content, publishedAt: now, source: 'atr' });
|
|
1421
1461
|
seeded++;
|
|
1422
1462
|
}
|
|
1463
|
+
console.log(` ${c.dim(` ATR: ${atrFiles.length} files processed`)}`);
|
|
1464
|
+
}
|
|
1465
|
+
catch (err) {
|
|
1466
|
+
console.error(` [WARN] ATR rule seeding failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1423
1467
|
}
|
|
1424
|
-
catch { /* no atr rules */ }
|
|
1425
1468
|
}
|
|
1426
1469
|
return seeded;
|
|
1427
1470
|
}
|