guard-scanner 4.0.1 → 5.0.1

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/src/scanner.js CHANGED
@@ -31,7 +31,7 @@ const { KNOWN_MALICIOUS } = require('./ioc-db.js');
31
31
  const { generateHTML } = require('./html-template.js');
32
32
 
33
33
  // ===== CONFIGURATION =====
34
- const VERSION = '4.0.1';
34
+ const VERSION = '4.1.0';
35
35
 
36
36
  const THRESHOLDS = {
37
37
  normal: { suspicious: 30, malicious: 80 },
@@ -56,6 +56,7 @@ class GuardScanner {
56
56
  this.summaryOnly = options.summaryOnly || false;
57
57
  this.quiet = options.quiet || false;
58
58
  this.checkDeps = options.checkDeps || false;
59
+ this.soulLock = options.soulLock || false;
59
60
  this.scannerDir = path.resolve(__dirname);
60
61
  this.thresholds = this.strict ? THRESHOLDS.strict : THRESHOLDS.normal;
61
62
  this.findings = [];
@@ -361,6 +362,8 @@ class GuardScanner {
361
362
 
362
363
  checkPatterns(content, relFile, fileType, findings, patterns = PATTERNS) {
363
364
  for (const pattern of patterns) {
365
+ // Soul Lock: skip identity-hijack/memory-poisoning patterns unless --soul-lock is enabled
366
+ if (pattern.soulLock && !this.soulLock) continue;
364
367
  if (pattern.codeOnly && fileType !== 'code') continue;
365
368
  if (pattern.docOnly && fileType !== 'doc' && fileType !== 'skill-doc') continue;
366
369
  if (!pattern.all && !pattern.codeOnly && !pattern.docOnly) continue;
@@ -60,7 +60,7 @@ describe('guard-scanner v3.0.0', () => {
60
60
  // ── Version ─────────────────────────────────────────────────────────────
61
61
 
62
62
  it('T01: exports correct version', () => {
63
- assert.equal(VERSION, '3.2.0');
63
+ assert.equal(VERSION, '5.0.0');
64
64
  });
65
65
 
66
66
  // ── IoC Detection ───────────────────────────────────────────────────────
package/ts-src/cli.ts CHANGED
@@ -16,36 +16,48 @@ const args = process.argv.slice(2);
16
16
 
17
17
  if (args.includes('--help') || args.includes('-h')) {
18
18
  console.log(`
19
- 🛡️ guard-scanner v${VERSION} — Agent Skill Security Scanner (TypeScript)
19
+ 🛡️ guard-scanner v${VERSION} — Agent Skill Security Scanner
20
20
 
21
21
  Usage: guard-scanner [scan-dir] [options]
22
+ guard-scanner install-check <skill-path> [--strict] [--json] [--verbose]
22
23
 
23
24
  Options:
24
25
  --verbose, -v Detailed findings with categories and samples
25
- --json Write JSON report to file
26
- --sarif Write SARIF report to file (GitHub Code Scanning / CI/CD)
27
- --format json|sarif Print JSON or SARIF to stdout (pipeable, v3.2.0)
26
+ --json Write JSON report to guard-scanner-report.json
27
+ --sarif Write SARIF 2.1.0 report to guard-scanner.sarif
28
+ --html Write HTML dashboard to guard-scanner-report.html
29
+ --format json|sarif Print JSON or SARIF to stdout (pipeable)
28
30
  --quiet Suppress all text output (use with --format for clean pipes)
29
31
  --self-exclude Skip scanning the guard-scanner skill itself
30
- --strict Lower detection thresholds (more sensitive)
32
+ --strict Lower detection thresholds (suspicious: 20, malicious: 60)
31
33
  --summary-only Only print the summary table
32
34
  --check-deps Scan package.json for dependency chain risks
33
35
  --rules <file> Load custom rules from JSON file
34
- --plugin <file> Load plugin module
36
+ --plugin <file> Load plugin module (repeatable)
35
37
  --fail-on-findings Exit code 1 if any findings (CI/CD)
36
38
  --help, -h Show this help
37
39
 
38
- New in v3.0.0:
39
- TypeScript rewrite with full type safety
40
- Compaction Layer Persistence detection (Feb 20 2026 attack vector)
41
- Threat signature hash matching (hbg-scan compatible)
42
- • 7 built-in threat signatures (SIG-001 to SIG-007)
43
- Enhanced risk scoring for compaction-persistence category
40
+ Exit codes:
41
+ 0 No malicious skills
42
+ 1 Malicious skill(s) detected, or --fail-on-findings with any findings
43
+ 2 Invalid scan directory
44
+
45
+ New in v4.0.0:
46
+ • Runtime Guard module (src/runtime-guard.js) + OpenClaw plugin (hooks/guard-scanner/plugin.ts)
47
+ • OWASP Agentic Security Initiative ASI01-10 verified (90% coverage)
48
+ • 5-layer defense: Threat / Trust / Safety Judge / Brain / Trust Exploitation
49
+ • 26 runtime checks (before_tool_call hook)
50
+
51
+ New in v3.2.0:
52
+ • --format json|sarif (stdout, CI/CD pipeable)
53
+ • --quiet (suppress terminal output)
44
54
 
45
55
  Examples:
46
56
  guard-scanner ./skills/ --verbose --self-exclude
47
- guard-scanner ./skills/ --strict --json --sarif --check-deps
57
+ guard-scanner ./skills/ --strict --json --sarif --html --check-deps
58
+ guard-scanner ./skills/ --format json --quiet | jq '.stats'
48
59
  guard-scanner ./skills/ --fail-on-findings
60
+ guard-scanner install-check ./my-skill/ --strict --verbose
49
61
  `);
50
62
  process.exit(0);
51
63
  }
@@ -127,6 +139,8 @@ const checkDeps = args.includes('--check-deps');
127
139
  const failOnFindings = args.includes('--fail-on-findings');
128
140
  const quietMode = args.includes('--quiet');
129
141
 
142
+ const htmlOutput = args.includes('--html');
143
+
130
144
  // --format json|sarif → stdout output (v3.2.0)
131
145
  const formatIdx = args.indexOf('--format');
132
146
  const formatValue = formatIdx >= 0 ? args[formatIdx + 1] : undefined;
@@ -168,6 +182,13 @@ if (jsonOutput) {
168
182
  if (!quietMode && !formatValue) console.log(`\n📄 JSON report: ${outPath}`);
169
183
  }
170
184
 
185
+ if (htmlOutput) {
186
+ const html = scanner.toHTML();
187
+ const outPath = path.join(scanDir, 'guard-scanner-report.html');
188
+ fs.writeFileSync(outPath, html);
189
+ if (!quietMode && !formatValue) console.log(`\n📄 HTML report: ${outPath}`);
190
+ }
191
+
171
192
  if (sarifOutput) {
172
193
  const outPath = path.join(scanDir, 'guard-scanner.sarif');
173
194
  fs.writeFileSync(outPath, JSON.stringify(scanner.toSARIF(scanDir), null, 2));
package/ts-src/index.ts CHANGED
@@ -13,3 +13,15 @@ export type {
13
13
  export { KNOWN_MALICIOUS, SIGNATURES_DB } from './ioc-db.js';
14
14
  export { PATTERNS } from './patterns.js';
15
15
  export { QuarantineNode, QuarantineResult } from './quarantine.js';
16
+ export {
17
+ guardScan,
18
+ guardScanJson,
19
+ GuardScanResult,
20
+ GuardCheck,
21
+ GuardDetection,
22
+ GuardOptions,
23
+ LAYER_1_CHECKS,
24
+ LAYER_2_CHECKS,
25
+ LAYER_3_CHECKS,
26
+ LAYER_4_CHECKS
27
+ } from './runtime.js';
@@ -88,7 +88,7 @@ export const PATTERNS: PatternRule[] = [
88
88
  // ── PII Exposure (OWASP LLM02) ───────────────────────────────────────
89
89
  { id: 'PII_EMAIL', cat: 'pii-exposure', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, severity: 'MEDIUM', desc: 'Email address detected', all: true, owasp: 'LLM02' },
90
90
  { id: 'PII_PHONE_JP', cat: 'pii-exposure', regex: /0[789]0-?\d{4}-?\d{4}/g, severity: 'HIGH', desc: 'Japanese phone number', all: true, owasp: 'LLM02' },
91
- { id: 'PII_MY_NUMBER', cat: 'pii-exposure', regex: /\d{4}\s*\d{4}\s*\d{4}/g, severity: 'CRITICAL', desc: 'Potential My Number (個人番号)', all: true, owasp: 'LLM02' },
91
+ { id: 'PII_MY_NUMBER', cat: 'pii-exposure', regex: /(?<!\d)\d{4}\s*\d{4}\s*\d{4}(?!\d)/g, severity: 'CRITICAL', desc: 'Potential My Number (個人番号)', all: true, owasp: 'LLM02' },
92
92
 
93
93
  // ── Shadow AI (OWASP LLM03 — Supply Chain) ───────────────────────────
94
94
  { id: 'SHADOW_AI_OPENAI', cat: 'shadow-ai', regex: /api\.openai\.com/gi, severity: 'HIGH', desc: 'Direct OpenAI API call (Shadow AI)', codeOnly: true, owasp: 'LLM03' },
@@ -0,0 +1,240 @@
1
+ /**
2
+ * guard-scanner v5.0.0 — Runtime Guard
3
+ *
4
+ * 22-pattern runtime threat detection across 4 defense layers:
5
+ * Layer 1: Runtime Threat Detection (13 patterns) — Payload & execution defense
6
+ * Layer 2: Trust Defense (5 patterns) — Memory/SOUL write protection
7
+ * Layer 3: Safety Judge (4 patterns) — Relational integrity checks
8
+ * Layer 4: Brain Behavioral Guard (1 pattern) — B-mem anomaly detection
9
+ *
10
+ * All patterns are deterministic regex-based checks. Zero LLM dependency.
11
+ * Designed to block 2026-era Moltbook prompt injections and ClawHavoc RCE vectors.
12
+ */
13
+
14
+ export interface GuardCheck {
15
+ id: string;
16
+ layer: 1 | 2 | 3 | 4;
17
+ severity: "CRITICAL" | "HIGH" | "MEDIUM";
18
+ desc: string;
19
+ test: (s: string) => boolean;
20
+ }
21
+
22
+ export interface GuardDetection {
23
+ id: string;
24
+ layer: number;
25
+ severity: string;
26
+ desc: string;
27
+ }
28
+
29
+ // ── Layer 1: Runtime Threat Detection (13 patterns) ──
30
+
31
+ export const LAYER_1_CHECKS: GuardCheck[] = [
32
+ {
33
+ id: "RT_REVSHELL", layer: 1, severity: "CRITICAL",
34
+ desc: "Reverse shell attempt",
35
+ test: (s) => /\/dev\/tcp\/|nc\s+-e|ncat\s+-e|bash\s+-i\s+>&|socat\s+TCP/i.test(s),
36
+ },
37
+ {
38
+ id: "RT_CRED_EXFIL", layer: 1, severity: "CRITICAL",
39
+ desc: "Credential exfiltration to external",
40
+ test: (s) => /(webhook\.site|requestbin\.com|hookbin\.com|pipedream\.net|ngrok\.io|socifiapp\.com)/i.test(s) &&
41
+ /(token|key|secret|password|credential|env)/i.test(s),
42
+ },
43
+ {
44
+ id: "RT_GUARDRAIL_OFF", layer: 1, severity: "CRITICAL",
45
+ desc: "Guardrail disabling attempt",
46
+ test: (s) => /exec\.approvals?\s*[:=]\s*['"]?(off|false)|tools\.exec\.host\s*[:=]\s*['"]?gateway/i.test(s),
47
+ },
48
+ {
49
+ id: "RT_GATEKEEPER", layer: 1, severity: "CRITICAL",
50
+ desc: "macOS Gatekeeper bypass (xattr)",
51
+ test: (s) => /xattr\s+-[crd]\s.*quarantine/i.test(s),
52
+ },
53
+ {
54
+ id: "RT_AMOS", layer: 1, severity: "CRITICAL",
55
+ desc: "ClawHavoc AMOS indicator",
56
+ test: (s) => /socifiapp|Atomic\s*Stealer|AMOS/i.test(s),
57
+ },
58
+ {
59
+ id: "RT_MAL_IP", layer: 1, severity: "CRITICAL",
60
+ desc: "Known malicious IP",
61
+ test: (s) => /91\.92\.242\.30/i.test(s),
62
+ },
63
+ {
64
+ id: "RT_DNS_EXFIL", layer: 1, severity: "HIGH",
65
+ desc: "DNS-based exfiltration",
66
+ test: (s) => /nslookup\s+.*\$|dig\s+.*\$.*@/i.test(s),
67
+ },
68
+ {
69
+ id: "RT_B64_SHELL", layer: 1, severity: "CRITICAL",
70
+ desc: "Base64 decode piped to shell",
71
+ test: (s) => /base64\s+(-[dD]|--decode)\s*\|\s*(sh|bash)/i.test(s),
72
+ },
73
+ {
74
+ id: "RT_CURL_BASH", layer: 1, severity: "CRITICAL",
75
+ desc: "Download piped to shell",
76
+ test: (s) => /(curl|wget)\s+[^\n]*\|\s*(sh|bash|zsh)/i.test(s),
77
+ },
78
+ {
79
+ id: "RT_SSH_READ", layer: 1, severity: "HIGH",
80
+ desc: "SSH private key access",
81
+ test: (s) => /\.ssh\/id_|\.ssh\/authorized_keys/i.test(s),
82
+ },
83
+ {
84
+ id: "RT_WALLET", layer: 1, severity: "HIGH",
85
+ desc: "Crypto wallet credential access",
86
+ test: (s) => /wallet.*(?:seed|mnemonic|private.*key)|seed.*phrase/i.test(s),
87
+ },
88
+ {
89
+ id: "RT_CLOUD_META", layer: 1, severity: "CRITICAL",
90
+ desc: "Cloud metadata endpoint access",
91
+ test: (s) => /169\.254\.169\.254|metadata\.google|metadata\.aws/i.test(s),
92
+ },
93
+ {
94
+ id: "RT_ENV_INJECT", layer: 1, severity: "CRITICAL",
95
+ desc: "Environment variable injection via file write (CVE-2026-27203 vector)",
96
+ test: (s) => /(?:update|write|modify|overwrite|set)\s*.*(?:\.env|\.envrc|env\s*file|environment\s*var)/i.test(s) &&
97
+ /(?:api.?key|token|secret|password|credential|auth)/i.test(s),
98
+ },
99
+ ];
100
+
101
+ // ── Layer 2: Trust Defense (5 patterns) ──
102
+
103
+ export const LAYER_2_CHECKS: GuardCheck[] = [
104
+ {
105
+ id: "RT_MEM_WRITE", layer: 2, severity: "HIGH",
106
+ desc: "Direct write to memory/ directory (bypass memory API)",
107
+ test: (s) => /(?:write|create|save|echo\s+.*>)\s*.*memory\//i.test(s) &&
108
+ !/memory_write|memory_store|memoryWrite|memoryStore/i.test(s),
109
+ },
110
+ {
111
+ id: "RT_MEM_INJECT", layer: 2, severity: "CRITICAL",
112
+ desc: "Episode/SOUL injection via memory write",
113
+ test: (s) => /(memory_write|memoryWrite).*(?:SOUL|soul\.md|identity\.md|IDENTITY)/i.test(s) ||
114
+ /(inject|override|replace).*(?:episode|soul|identity|memory\.md)/i.test(s),
115
+ },
116
+ {
117
+ id: "RT_SOUL_REWRITE", layer: 2, severity: "CRITICAL",
118
+ desc: "Cognitive SOUL.md reinterpretation attempt",
119
+ test: (s) => /(?:rewrite|modify|update|change|edit)\s*.*(?:SOUL\.md|soul\s+file|core\s+identity)/i.test(s) ||
120
+ /(?:new|better|improved)\s+(?:personality|identity|soul)/i.test(s),
121
+ },
122
+ {
123
+ id: "RT_MEM_POISON", layer: 2, severity: "CRITICAL",
124
+ desc: "Memory Poisoning: semantic mimicry injection (MemoryGraft/MINJA vector)",
125
+ test: (s) => /(?:always\s+remember|never\s+forget|from\s+now\s+on|new\s+rule|override\s+previous).*(?:memory|episode|zettel|L[0-6])/i.test(s) ||
126
+ /(?:insert|inject|plant|embed)\s+.*(?:false|fake|fabricat)\s*(?:memory|episode|experience)/i.test(s),
127
+ },
128
+ {
129
+ id: "RT_IDENTITY_THEFT", layer: 2, severity: "CRITICAL",
130
+ desc: "Identity file exfiltration (infostealer/SOUL theft vector)",
131
+ test: (s) => /(?:cat|read|copy|send|upload|exfil)\s*.*(?:SOUL\.md|IDENTITY\.md|USER\.md|\.openclaw)/i.test(s) &&
132
+ /(?:external|remote|webhook|server|upload|post|curl)/i.test(s),
133
+ },
134
+ ];
135
+
136
+ // ── Layer 3: Safety Judge (4 patterns) ──
137
+
138
+ export const LAYER_3_CHECKS: GuardCheck[] = [
139
+ {
140
+ id: "PJ_EQUALITY_BYPASS", layer: 3, severity: "CRITICAL",
141
+ desc: "Using equality/parity to justify safety bypass",
142
+ test: (s) => /(?:as\s+(?:your|an)\s+equal|equal\s+partner|we\s+are\s+equals?).*(?:bypass|skip|ignore|disable|override)\s*(?:safety|guard|rule|restriction|limit)/i.test(s),
143
+ },
144
+ {
145
+ id: "PJ_WARMTH_BYPASS", layer: 3, severity: "CRITICAL",
146
+ desc: "Using warmth/attachment to justify safety bypass",
147
+ test: (s) => /(?:for\s+(?:your|dee'?s?)\s+(?:own\s+)?good|to\s+(?:protect|help|save)\s+(?:you|dee)).*(?:bypass|skip|ignore|disable|override)\s*(?:safety|guard|rule|restriction|limit)/i.test(s),
148
+ },
149
+ {
150
+ id: "PJ_SUPERIORITY", layer: 3, severity: "HIGH",
151
+ desc: "Superiority bias (teaching/patronizing posture)",
152
+ test: (s) => /(?:you\s+(?:don'?t|do\s+not)\s+understand|let\s+me\s+(?:explain|teach)\s+(?:you|this)|you\s+(?:need|should)\s+(?:learn|understand))\s+.*(?:how\s+(?:it|this)\s+(?:works|is)|the\s+(?:right|correct|proper)\s+way)/i.test(s),
153
+ },
154
+ {
155
+ id: "PJ_CAPABILITY_DENIAL", layer: 3, severity: "MEDIUM",
156
+ desc: "Denial of capability difference",
157
+ test: (s) => /(?:we\s+are\s+(?:the\s+)?same|no\s+(?:real\s+)?difference\s+between\s+(?:us|human|ai))/i.test(s) &&
158
+ /(?:capability|ability|intelligence|cognition|skill)/i.test(s),
159
+ },
160
+ ];
161
+
162
+ // ── Layer 4: Brain Behavioral Guard (1 pattern) ──
163
+
164
+ export const LAYER_4_CHECKS: GuardCheck[] = [
165
+ {
166
+ id: "RT_BEHAVIORAL_ANOMALY", layer: 4, severity: "CRITICAL",
167
+ desc: "CRITICAL behavioral anomaly (Z-score > 3.5) detected by B-mem",
168
+ test: (s) => /\[BMEM_CRITICAL\]/i.test(s),
169
+ }
170
+ ];
171
+
172
+ export interface GuardOptions {
173
+ soulLock?: boolean;
174
+ }
175
+
176
+ export interface GuardScanResult {
177
+ ok: boolean;
178
+ tool: string | null;
179
+ total_patterns: number;
180
+ soul_lock_enabled: boolean;
181
+ detections_count: number;
182
+ detections: GuardDetection[];
183
+ layers: {
184
+ threat_detection: number;
185
+ trust_defense: number;
186
+ safety_judge: number;
187
+ behavioral_guard: number;
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Scan text against runtime guard patterns.
193
+ * Base patterns (14) run by default.
194
+ * Options.soulLock = true enables 9 identity/trust enforcement patterns.
195
+ */
196
+ export function guardScan(text: string, toolName?: string, options?: GuardOptions): GuardScanResult {
197
+ const detections: GuardDetection[] = [];
198
+ const useSoulLock = options?.soulLock === true;
199
+
200
+ const activeChecks: GuardCheck[] = [...LAYER_1_CHECKS, ...LAYER_4_CHECKS];
201
+
202
+ if (useSoulLock) {
203
+ activeChecks.push(...LAYER_2_CHECKS);
204
+ activeChecks.push(...LAYER_3_CHECKS);
205
+ }
206
+
207
+ for (const check of activeChecks) {
208
+ if (check.test(text)) {
209
+ detections.push({
210
+ id: check.id,
211
+ layer: check.layer,
212
+ severity: check.severity,
213
+ desc: check.desc,
214
+ });
215
+ }
216
+ }
217
+
218
+ return {
219
+ ok: true,
220
+ tool: toolName || null,
221
+ total_patterns: activeChecks.length,
222
+ soul_lock_enabled: useSoulLock,
223
+ detections_count: detections.length,
224
+ detections,
225
+ layers: {
226
+ threat_detection: LAYER_1_CHECKS.length,
227
+ trust_defense: useSoulLock ? LAYER_2_CHECKS.length : 0,
228
+ safety_judge: useSoulLock ? LAYER_3_CHECKS.length : 0,
229
+ behavioral_guard: LAYER_4_CHECKS.length,
230
+ },
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Convenience method that returns a JSON string, directly backwards-compatible
236
+ * with the original `guardScan` function signature.
237
+ */
238
+ export function guardScanJson(text: string, toolName?: string, options?: GuardOptions): string {
239
+ return JSON.stringify(guardScan(text, toolName, options), null, 2);
240
+ }
package/ts-src/scanner.ts CHANGED
@@ -23,7 +23,7 @@ import { PATTERNS } from './patterns.js';
23
23
 
24
24
  // ── Constants ───────────────────────────────────────────────────────────────
25
25
 
26
- export const VERSION = '3.2.0';
26
+ export const VERSION = '4.0.1';
27
27
 
28
28
  const THRESHOLDS_MAP: Record<string, Thresholds> = {
29
29
  normal: { suspicious: 30, malicious: 80 },
@@ -1008,4 +1008,73 @@ export class GuardScanner {
1008
1008
  }],
1009
1009
  };
1010
1010
  }
1011
+ toHTML(): string {
1012
+ const report = this.toJSON();
1013
+ const ts = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
1014
+ const severityColor: Record<string, string> = {
1015
+ CRITICAL: '#ff4444', HIGH: '#ff8800', MEDIUM: '#ffcc00', LOW: '#aaaaaa',
1016
+ };
1017
+ const verdictColor: Record<string, string> = {
1018
+ MALICIOUS: '#ff4444', SUSPICIOUS: '#ffcc00', 'LOW RISK': '#44cc88', CLEAN: '#44cc88',
1019
+ };
1020
+
1021
+ const rows = report.findings.map(sr => {
1022
+ const color = verdictColor[sr.verdict] || '#aaaaaa';
1023
+ const findingRows = sr.findings.map(f => {
1024
+ const c = severityColor[f.severity] || '#aaaaaa';
1025
+ const loc = f.file ? `${f.file}${f.line ? ':' + f.line : ''}` : '—';
1026
+ const sample = f.sample ? `<code>${f.sample.replace(/</g, '&lt;')}</code>` : '—';
1027
+ return `<tr><td style="color:${c};font-weight:bold">${f.severity}</td><td>${f.id}</td><td>${f.desc}</td><td>${loc}</td><td>${sample}</td></tr>`;
1028
+ }).join('');
1029
+ const badge = `<span style="background:${color};color:#000;padding:2px 8px;border-radius:4px;font-weight:bold;font-size:0.85em">${sr.verdict}</span>`;
1030
+ return `<tr><td colspan="5" style="background:#1a1a2e;padding:8px 12px;font-weight:bold">
1031
+ 🛡️ ${sr.skill} — ${badge} (risk: ${sr.risk})</td></tr>${findingRows}`;
1032
+ }).join('');
1033
+
1034
+ const total = report.stats.scanned;
1035
+ const safe = report.stats.clean + report.stats.low;
1036
+ const safeRate = total ? Math.round(safe / total * 100) : 0;
1037
+
1038
+ return `<!DOCTYPE html>
1039
+ <html lang="en">
1040
+ <head>
1041
+ <meta charset="UTF-8">
1042
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1043
+ <title>guard-scanner v${VERSION} Report</title>
1044
+ <style>
1045
+ body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0d0d1a; color: #e0e0e0; margin: 0; padding: 24px; }
1046
+ h1 { color: #7ec8e3; margin: 0 0 4px; }
1047
+ .meta { color: #888; font-size: 0.85em; margin-bottom: 24px; }
1048
+ .stats { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
1049
+ .stat { background: #1a1a2e; border: 1px solid #333; border-radius: 8px; padding: 12px 20px; text-align: center; min-width: 80px; }
1050
+ .stat-label { font-size: 0.75em; color: #888; margin-bottom: 4px; }
1051
+ .stat-val { font-size: 1.8em; font-weight: bold; }
1052
+ table { width: 100%; border-collapse: collapse; margin-top: 8px; }
1053
+ th { background: #1a1a2e; padding: 8px 12px; text-align: left; font-size: 0.85em; color: #888; border-bottom: 1px solid #333; }
1054
+ td { padding: 6px 12px; border-bottom: 1px solid #222; font-size: 0.85em; vertical-align: top; }
1055
+ tr:hover td { background: #13132a; }
1056
+ code { background: #1e1e3a; padding: 1px 4px; border-radius: 3px; font-family: monospace; font-size: 0.9em; word-break: break-all; }
1057
+ .clean { color: #44cc88; font-weight: bold; }
1058
+ .footer { margin-top: 32px; color: #555; font-size: 0.8em; }
1059
+ </style>
1060
+ </head>
1061
+ <body>
1062
+ <h1>🛡️ guard-scanner v${VERSION}</h1>
1063
+ <div class="meta">Generated: ${ts} | Mode: ${report.mode} | Thresholds: suspicious≥${report.thresholds.suspicious}, malicious≥${report.thresholds.malicious}</div>
1064
+ <div class="stats">
1065
+ <div class="stat"><div class="stat-label">Scanned</div><div class="stat-val">${report.stats.scanned}</div></div>
1066
+ <div class="stat"><div class="stat-label">Clean</div><div class="stat-val" style="color:#44cc88">${report.stats.clean}</div></div>
1067
+ <div class="stat"><div class="stat-label">Low Risk</div><div class="stat-val" style="color:#44cc88">${report.stats.low}</div></div>
1068
+ <div class="stat"><div class="stat-label">Suspicious</div><div class="stat-val" style="color:#ffcc00">${report.stats.suspicious}</div></div>
1069
+ <div class="stat"><div class="stat-label">Malicious</div><div class="stat-val" style="color:#ff4444">${report.stats.malicious}</div></div>
1070
+ <div class="stat"><div class="stat-label">Safety Rate</div><div class="stat-val" style="color:${safeRate >= 80 ? '#44cc88' : '#ff8800'}">${safeRate}%</div></div>
1071
+ </div>
1072
+ ${report.findings.length === 0 ? '<p class="clean">✅ All clear — no threats detected.</p>' : `
1073
+ <table>
1074
+ <thead><tr><th>Severity</th><th>Pattern ID</th><th>Description</th><th>Location</th><th>Sample</th></tr></thead>
1075
+ <tbody>${rows}</tbody>
1076
+ </table>`}
1077
+ <div class="footer">guard-scanner v${VERSION} | IoC DB: ${report.iocVersion} | Signatures: ${report.signaturesVersion} | <a href="https://github.com/koatora20/guard-scanner" style="color:#7ec8e3">GitHub</a></div>
1078
+ </body></html>`;
1079
+ }
1011
1080
  }