@guava-parity/guard-scanner 9.1.0 → 15.0.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 (79) hide show
  1. package/README.md +42 -253
  2. package/SECURITY.md +12 -4
  3. package/SKILL.md +121 -59
  4. package/dist/openclaw-plugin.mjs +41 -0
  5. package/docs/EVIDENCE_DRIVEN.md +182 -0
  6. package/docs/banner.png +0 -0
  7. package/docs/data/corpus-metrics.json +11 -0
  8. package/docs/data/latest.json +29845 -0
  9. package/docs/generated/npm-audit-20260312.json +96 -0
  10. package/docs/generated/openclaw-upstream-status.json +25 -0
  11. package/docs/glossary.md +46 -0
  12. package/docs/index.html +1119 -0
  13. package/docs/logo.png +0 -0
  14. package/docs/openclaw-compatibility-audit.md +44 -0
  15. package/docs/openclaw-continuous-compatibility-plan.md +36 -0
  16. package/docs/rules/a2a-contagion.md +68 -0
  17. package/docs/rules/advanced-exfil.md +52 -0
  18. package/docs/rules/agent-protocol.md +108 -0
  19. package/docs/rules/api-abuse.md +68 -0
  20. package/docs/rules/autonomous-risk.md +92 -0
  21. package/docs/rules/config-impact.md +132 -0
  22. package/docs/rules/credential-handling.md +100 -0
  23. package/docs/rules/cve-patterns.md +332 -0
  24. package/docs/rules/data-exposure.md +84 -0
  25. package/docs/rules/exfiltration.md +36 -0
  26. package/docs/rules/financial-access.md +84 -0
  27. package/docs/rules/identity-hijack.md +140 -0
  28. package/docs/rules/inference-manipulation.md +60 -0
  29. package/docs/rules/leaky-skills.md +52 -0
  30. package/docs/rules/malicious-code.md +108 -0
  31. package/docs/rules/mcp-security.md +148 -0
  32. package/docs/rules/memory-poisoning.md +84 -0
  33. package/docs/rules/model-poisoning.md +44 -0
  34. package/docs/rules/obfuscation.md +60 -0
  35. package/docs/rules/persistence.md +108 -0
  36. package/docs/rules/pii-exposure.md +116 -0
  37. package/docs/rules/prompt-injection.md +148 -0
  38. package/docs/rules/prompt-worm.md +44 -0
  39. package/docs/rules/safeguard-bypass.md +44 -0
  40. package/docs/rules/sandbox-escape.md +100 -0
  41. package/docs/rules/secret-detection.md +44 -0
  42. package/docs/rules/supply-chain-v2.md +92 -0
  43. package/docs/rules/suspicious-download.md +60 -0
  44. package/docs/rules/trust-boundary.md +76 -0
  45. package/docs/rules/trust-exploitation.md +92 -0
  46. package/docs/rules/unverifiable-deps.md +84 -0
  47. package/docs/rules/vdb-injection.md +84 -0
  48. package/docs/security-vulnerability-report-20260312.md +53 -0
  49. package/docs/spec/PRD_V2_ARCHITECTURE.md +55 -0
  50. package/docs/spec/capabilities.json +42 -0
  51. package/docs/spec/finding.schema.json +104 -0
  52. package/docs/spec/integration-manifest.md +39 -0
  53. package/docs/spec/sbom.json +33 -0
  54. package/docs/threat-model.md +65 -0
  55. package/docs/v13-architecture-manifest.md +55 -0
  56. package/hooks/context.js +305 -0
  57. package/hooks/guard-scanner/plugin.ts +24 -1
  58. package/openclaw-plugin.mts +91 -0
  59. package/openclaw.plugin.json +30 -53
  60. package/package.json +80 -57
  61. package/src/cli.js +174 -34
  62. package/src/core/content-loader.js +42 -0
  63. package/src/core/inventory.js +73 -0
  64. package/src/core/report-adapters.js +171 -0
  65. package/src/core/risk-engine.js +93 -0
  66. package/src/core/rule-registry.js +73 -0
  67. package/src/core/semantic-validators.js +85 -0
  68. package/src/finding-schema.js +191 -0
  69. package/src/hooks/context.ts +49 -0
  70. package/src/html-template.js +2 -2
  71. package/src/mcp-server.js +192 -5
  72. package/src/openclaw-upstream.js +128 -0
  73. package/src/patterns.js +519 -157
  74. package/src/policy-engine.js +32 -0
  75. package/src/runtime-guard.js +40 -2
  76. package/src/scanner.js +228 -231
  77. package/src/skill-crawler.js +254 -0
  78. package/src/threat-model.js +50 -0
  79. package/src/validation-layer.js +39 -0
@@ -0,0 +1,1119 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>guard-scanner — Security Dashboard</title>
8
+ <meta name="description"
9
+ content="Real-time security dashboard for AI agent skills. 352 patterns · 32 categories · 8 MCP checks · OWASP ASI01-10. Zero dependencies.">
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link
13
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap"
14
+ rel="stylesheet">
15
+ <style>
16
+ *,
17
+ *::before,
18
+ *::after {
19
+ box-sizing: border-box;
20
+ margin: 0;
21
+ padding: 0
22
+ }
23
+
24
+ :root {
25
+ --bg: #0a0e17;
26
+ --bg2: #111827;
27
+ --surface: rgba(17, 24, 39, .65);
28
+ --border: rgba(0, 255, 136, .12);
29
+ --border-hover: rgba(0, 255, 136, .3);
30
+ --green: #00ff88;
31
+ --green-dim: rgba(0, 255, 136, .15);
32
+ --green-glow: rgba(0, 255, 136, .25);
33
+ --amber: #ffb800;
34
+ --amber-dim: rgba(255, 184, 0, .15);
35
+ --red: #ff3b5c;
36
+ --red-dim: rgba(255, 59, 92, .15);
37
+ --blue: #38bdf8;
38
+ --blue-dim: rgba(56, 189, 248, .12);
39
+ --purple: #a78bfa;
40
+ --purple-dim: rgba(167, 139, 250, .12);
41
+ --text: #e2e8f0;
42
+ --text-dim: #94a3b8;
43
+ --text-muted: #64748b;
44
+ --font: 'Inter', system-ui, sans-serif;
45
+ --mono: 'JetBrains Mono', monospace;
46
+ --glass: saturate(180%) blur(20px);
47
+ --radius: 16px;
48
+ --radius-sm: 10px;
49
+ }
50
+
51
+ html {
52
+ scroll-behavior: smooth
53
+ }
54
+
55
+ body {
56
+ font-family: var(--font);
57
+ background: var(--bg);
58
+ color: var(--text);
59
+ min-height: 100vh;
60
+ overflow-x: hidden;
61
+ line-height: 1.6
62
+ }
63
+
64
+ /* ── Background Effects ── */
65
+ body::before {
66
+ content: '';
67
+ position: fixed;
68
+ top: -50%;
69
+ left: -50%;
70
+ width: 200%;
71
+ height: 200%;
72
+ background: radial-gradient(ellipse at 20% 50%, rgba(0, 255, 136, .04) 0%, transparent 50%), radial-gradient(ellipse at 80% 20%, rgba(56, 189, 248, .03) 0%, transparent 50%), radial-gradient(ellipse at 50% 80%, rgba(167, 139, 250, .03) 0%, transparent 40%);
73
+ z-index: 0;
74
+ pointer-events: none
75
+ }
76
+
77
+ .grid-bg {
78
+ position: fixed;
79
+ inset: 0;
80
+ background-image: linear-gradient(rgba(0, 255, 136, .03) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 255, 136, .03) 1px, transparent 1px);
81
+ background-size: 64px 64px;
82
+ z-index: 0;
83
+ pointer-events: none;
84
+ opacity: .5
85
+ }
86
+
87
+ /* ── Layout ── */
88
+ .container {
89
+ max-width: 1200px;
90
+ margin: 0 auto;
91
+ padding: 0 24px;
92
+ position: relative;
93
+ z-index: 1
94
+ }
95
+
96
+ section {
97
+ margin-bottom: 64px
98
+ }
99
+
100
+ /* ── Animations ── */
101
+ @keyframes fadeUp {
102
+ from {
103
+ opacity: 0;
104
+ transform: translateY(24px)
105
+ }
106
+
107
+ to {
108
+ opacity: 1;
109
+ transform: translateY(0)
110
+ }
111
+ }
112
+
113
+ @keyframes pulse-glow {
114
+
115
+ 0%,
116
+ 100% {
117
+ box-shadow: 0 0 20px rgba(0, 255, 136, .1)
118
+ }
119
+
120
+ 50% {
121
+ box-shadow: 0 0 40px rgba(0, 255, 136, .2)
122
+ }
123
+ }
124
+
125
+ @keyframes count-up {
126
+ from {
127
+ opacity: .3
128
+ }
129
+
130
+ to {
131
+ opacity: 1
132
+ }
133
+ }
134
+
135
+ .fade-up {
136
+ animation: fadeUp .6s ease-out both
137
+ }
138
+
139
+ .fade-up:nth-child(2) {
140
+ animation-delay: .1s
141
+ }
142
+
143
+ .fade-up:nth-child(3) {
144
+ animation-delay: .2s
145
+ }
146
+
147
+ .fade-up:nth-child(4) {
148
+ animation-delay: .3s
149
+ }
150
+
151
+ /* ── Glass Card ── */
152
+ .glass {
153
+ background: var(--surface);
154
+ backdrop-filter: var(--glass);
155
+ -webkit-backdrop-filter: var(--glass);
156
+ border: 1px solid var(--border);
157
+ border-radius: var(--radius);
158
+ transition: border-color .3s, box-shadow .3s, transform .3s
159
+ }
160
+
161
+ .glass:hover {
162
+ border-color: var(--border-hover);
163
+ box-shadow: 0 8px 32px rgba(0, 255, 136, .08);
164
+ transform: translateY(-2px)
165
+ }
166
+
167
+ /* ── Hero ── */
168
+ .hero {
169
+ padding: 80px 0 48px;
170
+ text-align: center
171
+ }
172
+
173
+ .hero-badge {
174
+ display: inline-flex;
175
+ align-items: center;
176
+ gap: 8px;
177
+ padding: 6px 16px;
178
+ border-radius: 999px;
179
+ background: var(--green-dim);
180
+ border: 1px solid rgba(0, 255, 136, .2);
181
+ font-size: 13px;
182
+ font-weight: 600;
183
+ color: var(--green);
184
+ letter-spacing: .03em;
185
+ margin-bottom: 24px
186
+ }
187
+
188
+ .hero-badge .dot {
189
+ width: 8px;
190
+ height: 8px;
191
+ border-radius: 50%;
192
+ background: var(--green);
193
+ animation: pulse-glow 2s ease-in-out infinite
194
+ }
195
+
196
+ .hero h1 {
197
+ font-size: clamp(2.5rem, 6vw, 4rem);
198
+ font-weight: 900;
199
+ letter-spacing: -.03em;
200
+ background: linear-gradient(135deg, #fff 0%, var(--green) 50%, var(--blue) 100%);
201
+ -webkit-background-clip: text;
202
+ -webkit-text-fill-color: transparent;
203
+ background-clip: text;
204
+ margin-bottom: 12px
205
+ }
206
+
207
+ .hero h1 .shield {
208
+ font-size: 1em;
209
+ -webkit-text-fill-color: var(--green);
210
+ filter: drop-shadow(0 0 12px rgba(0, 255, 136, .4))
211
+ }
212
+
213
+ .hero .tagline {
214
+ font-size: clamp(1.1rem, 2.5vw, 1.4rem);
215
+ color: var(--text-dim);
216
+ font-weight: 500;
217
+ max-width: 640px;
218
+ margin: 0 auto 32px
219
+ }
220
+
221
+ .hero .badges {
222
+ display: flex;
223
+ flex-wrap: wrap;
224
+ gap: 8px;
225
+ justify-content: center
226
+ }
227
+
228
+ .hero .badges img {
229
+ height: 22px;
230
+ border-radius: 4px
231
+ }
232
+
233
+ .hero .sub-stats {
234
+ display: flex;
235
+ flex-wrap: wrap;
236
+ gap: 24px;
237
+ justify-content: center;
238
+ margin-top: 24px;
239
+ font-size: 14px;
240
+ color: var(--text-muted)
241
+ }
242
+
243
+ .hero .sub-stats span {
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 6px
247
+ }
248
+
249
+ .hero .sub-stats .num {
250
+ color: var(--green);
251
+ font-family: var(--mono);
252
+ font-weight: 700
253
+ }
254
+
255
+ /* ── Stats Grid ── */
256
+ .stats-grid {
257
+ display: grid;
258
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
259
+ gap: 20px;
260
+ margin-bottom: 48px
261
+ }
262
+
263
+ .stat-card {
264
+ padding: 28px 24px;
265
+ text-align: center
266
+ }
267
+
268
+ .stat-card .stat-icon {
269
+ font-size: 28px;
270
+ margin-bottom: 12px;
271
+ display: block
272
+ }
273
+
274
+ .stat-card .stat-value {
275
+ font-family: var(--mono);
276
+ font-size: 2.4rem;
277
+ font-weight: 800;
278
+ line-height: 1;
279
+ margin-bottom: 6px;
280
+ background: linear-gradient(135deg, #fff, var(--green));
281
+ -webkit-background-clip: text;
282
+ -webkit-text-fill-color: transparent;
283
+ background-clip: text
284
+ }
285
+
286
+ .stat-card .stat-label {
287
+ font-size: 13px;
288
+ color: var(--text-muted);
289
+ text-transform: uppercase;
290
+ letter-spacing: .08em;
291
+ font-weight: 600
292
+ }
293
+
294
+ .stat-card.amber .stat-value {
295
+ background: linear-gradient(135deg, #fff, var(--amber));
296
+ -webkit-background-clip: text;
297
+ -webkit-text-fill-color: transparent;
298
+ background-clip: text
299
+ }
300
+
301
+ .stat-card.red .stat-value {
302
+ background: linear-gradient(135deg, #fff, var(--red));
303
+ -webkit-background-clip: text;
304
+ -webkit-text-fill-color: transparent;
305
+ background-clip: text
306
+ }
307
+
308
+ .stat-card.blue .stat-value {
309
+ background: linear-gradient(135deg, #fff, var(--blue));
310
+ -webkit-background-clip: text;
311
+ -webkit-text-fill-color: transparent;
312
+ background-clip: text
313
+ }
314
+
315
+ /* ── Section Headings ── */
316
+ .section-head {
317
+ margin-bottom: 28px
318
+ }
319
+
320
+ .section-head h2 {
321
+ font-size: 1.6rem;
322
+ font-weight: 800;
323
+ letter-spacing: -.02em;
324
+ display: flex;
325
+ align-items: center;
326
+ gap: 10px
327
+ }
328
+
329
+ .section-head h2 .icon {
330
+ font-size: 1.2em
331
+ }
332
+
333
+ .section-head p {
334
+ color: var(--text-dim);
335
+ font-size: 14px;
336
+ margin-top: 4px;
337
+ max-width: 600px
338
+ }
339
+
340
+ /* ── Checks Grid ── */
341
+ .checks-grid {
342
+ display: grid;
343
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
344
+ gap: 16px
345
+ }
346
+
347
+ .check-card {
348
+ padding: 20px;
349
+ display: flex;
350
+ align-items: flex-start;
351
+ gap: 14px
352
+ }
353
+
354
+ .check-card .check-num {
355
+ font-family: var(--mono);
356
+ font-size: 12px;
357
+ font-weight: 700;
358
+ color: var(--green);
359
+ background: var(--green-dim);
360
+ width: 28px;
361
+ height: 28px;
362
+ border-radius: 8px;
363
+ display: flex;
364
+ align-items: center;
365
+ justify-content: center;
366
+ flex-shrink: 0
367
+ }
368
+
369
+ .check-card .check-body h3 {
370
+ font-size: 14px;
371
+ font-weight: 700;
372
+ margin-bottom: 4px;
373
+ color: #fff
374
+ }
375
+
376
+ .check-card .check-body p {
377
+ font-size: 12px;
378
+ color: var(--text-muted);
379
+ line-height: 1.5
380
+ }
381
+
382
+ .check-card .check-body .ref {
383
+ font-size: 11px;
384
+ color: var(--blue);
385
+ font-family: var(--mono);
386
+ margin-top: 4px;
387
+ opacity: .8
388
+ }
389
+
390
+ /* ── OWASP Grid ── */
391
+ .owasp-grid {
392
+ display: grid;
393
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
394
+ gap: 12px
395
+ }
396
+
397
+ .owasp-item {
398
+ padding: 16px;
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 12px
402
+ }
403
+
404
+ .owasp-item .owasp-code {
405
+ font-family: var(--mono);
406
+ font-size: 12px;
407
+ font-weight: 700;
408
+ color: var(--green);
409
+ background: var(--green-dim);
410
+ padding: 4px 10px;
411
+ border-radius: 6px;
412
+ white-space: nowrap
413
+ }
414
+
415
+ .owasp-item .owasp-label {
416
+ font-size: 13px;
417
+ font-weight: 500
418
+ }
419
+
420
+ .owasp-item .owasp-check {
421
+ margin-left: auto;
422
+ color: var(--green);
423
+ font-size: 16px;
424
+ flex-shrink: 0
425
+ }
426
+
427
+ /* ── Donut Chart ── */
428
+ .chart-container {
429
+ display: flex;
430
+ flex-wrap: wrap;
431
+ gap: 40px;
432
+ align-items: center;
433
+ justify-content: center
434
+ }
435
+
436
+ .donut-wrap {
437
+ position: relative;
438
+ width: 200px;
439
+ height: 200px;
440
+ flex-shrink: 0
441
+ }
442
+
443
+ .donut-wrap svg {
444
+ width: 100%;
445
+ height: 100%;
446
+ transform: rotate(-90deg)
447
+ }
448
+
449
+ .donut-wrap .center-label {
450
+ position: absolute;
451
+ inset: 0;
452
+ display: flex;
453
+ flex-direction: column;
454
+ align-items: center;
455
+ justify-content: center
456
+ }
457
+
458
+ .donut-wrap .center-label .big {
459
+ font-family: var(--mono);
460
+ font-size: 2rem;
461
+ font-weight: 800;
462
+ color: #fff
463
+ }
464
+
465
+ .donut-wrap .center-label .sub {
466
+ font-size: 12px;
467
+ color: var(--text-muted);
468
+ margin-top: 2px
469
+ }
470
+
471
+ .legend {
472
+ display: flex;
473
+ flex-direction: column;
474
+ gap: 8px
475
+ }
476
+
477
+ .legend-item {
478
+ display: flex;
479
+ align-items: center;
480
+ gap: 10px;
481
+ font-size: 13px
482
+ }
483
+
484
+ .legend-item .swatch {
485
+ width: 12px;
486
+ height: 12px;
487
+ border-radius: 3px;
488
+ flex-shrink: 0
489
+ }
490
+
491
+ .legend-item .count {
492
+ font-family: var(--mono);
493
+ margin-left: auto;
494
+ color: var(--text-dim);
495
+ font-weight: 600;
496
+ min-width: 24px;
497
+ text-align: right
498
+ }
499
+
500
+ /* ── Skills Table ── */
501
+ .table-wrap {
502
+ overflow-x: auto;
503
+ border-radius: var(--radius)
504
+ }
505
+
506
+ .table-controls {
507
+ display: flex;
508
+ flex-wrap: wrap;
509
+ gap: 12px;
510
+ margin-bottom: 16px;
511
+ align-items: center
512
+ }
513
+
514
+ .table-controls input {
515
+ background: var(--surface);
516
+ backdrop-filter: var(--glass);
517
+ border: 1px solid var(--border);
518
+ border-radius: var(--radius-sm);
519
+ padding: 10px 16px;
520
+ color: var(--text);
521
+ font-size: 14px;
522
+ font-family: var(--font);
523
+ width: 280px;
524
+ outline: none;
525
+ transition: border-color .3s
526
+ }
527
+
528
+ .table-controls input:focus {
529
+ border-color: var(--green)
530
+ }
531
+
532
+ .table-controls input::placeholder {
533
+ color: var(--text-muted)
534
+ }
535
+
536
+ .filter-btn {
537
+ background: var(--surface);
538
+ border: 1px solid var(--border);
539
+ border-radius: var(--radius-sm);
540
+ padding: 8px 16px;
541
+ color: var(--text-dim);
542
+ font-size: 13px;
543
+ font-weight: 600;
544
+ cursor: pointer;
545
+ transition: all .2s;
546
+ font-family: var(--font)
547
+ }
548
+
549
+ .filter-btn:hover,
550
+ .filter-btn.active {
551
+ border-color: var(--green);
552
+ color: var(--green);
553
+ background: var(--green-dim)
554
+ }
555
+
556
+ table {
557
+ width: 100%;
558
+ border-collapse: collapse
559
+ }
560
+
561
+ table th {
562
+ text-align: left;
563
+ padding: 14px 16px;
564
+ font-size: 12px;
565
+ text-transform: uppercase;
566
+ letter-spacing: .06em;
567
+ color: var(--text-muted);
568
+ font-weight: 700;
569
+ border-bottom: 1px solid var(--border);
570
+ cursor: pointer;
571
+ user-select: none;
572
+ white-space: nowrap;
573
+ transition: color .2s
574
+ }
575
+
576
+ table th:hover {
577
+ color: var(--green)
578
+ }
579
+
580
+ table td {
581
+ padding: 12px 16px;
582
+ font-size: 14px;
583
+ border-bottom: 1px solid rgba(255, 255, 255, .04);
584
+ vertical-align: middle
585
+ }
586
+
587
+ table tr {
588
+ transition: background .2s
589
+ }
590
+
591
+ table tbody tr:hover {
592
+ background: rgba(0, 255, 136, .03)
593
+ }
594
+
595
+ .skill-name {
596
+ font-weight: 600;
597
+ font-family: var(--mono);
598
+ font-size: 13px
599
+ }
600
+
601
+ .badge {
602
+ display: inline-block;
603
+ padding: 3px 10px;
604
+ border-radius: 6px;
605
+ font-size: 11px;
606
+ font-weight: 700;
607
+ font-family: var(--mono);
608
+ text-transform: uppercase;
609
+ letter-spacing: .04em
610
+ }
611
+
612
+ .badge-clean {
613
+ background: var(--green-dim);
614
+ color: var(--green)
615
+ }
616
+
617
+ .badge-findings {
618
+ background: var(--amber-dim);
619
+ color: var(--amber)
620
+ }
621
+
622
+ .badge-error {
623
+ background: var(--red-dim);
624
+ color: var(--red)
625
+ }
626
+
627
+ .risk-bar {
628
+ width: 60px;
629
+ height: 6px;
630
+ background: rgba(255, 255, 255, .08);
631
+ border-radius: 3px;
632
+ overflow: hidden;
633
+ display: inline-block;
634
+ vertical-align: middle;
635
+ margin-right: 8px
636
+ }
637
+
638
+ .risk-bar-fill {
639
+ height: 100%;
640
+ border-radius: 3px;
641
+ transition: width .4s ease
642
+ }
643
+
644
+ .risk-low {
645
+ background: var(--green)
646
+ }
647
+
648
+ .risk-med {
649
+ background: var(--amber)
650
+ }
651
+
652
+ .risk-high {
653
+ background: var(--red)
654
+ }
655
+
656
+ /* ── Footer ── */
657
+ .footer {
658
+ text-align: center;
659
+ padding: 48px 0;
660
+ border-top: 1px solid var(--border);
661
+ color: var(--text-muted);
662
+ font-size: 13px
663
+ }
664
+
665
+ .footer a {
666
+ color: var(--green);
667
+ text-decoration: none;
668
+ font-weight: 600;
669
+ transition: opacity .2s
670
+ }
671
+
672
+ .footer a:hover {
673
+ opacity: .7
674
+ }
675
+
676
+ .footer .footer-links {
677
+ display: flex;
678
+ flex-wrap: wrap;
679
+ gap: 24px;
680
+ justify-content: center;
681
+ margin-bottom: 16px
682
+ }
683
+
684
+ .footer .heart {
685
+ color: var(--red)
686
+ }
687
+
688
+ /* ── Responsive ── */
689
+ @media(max-width:768px) {
690
+ .hero {
691
+ padding: 48px 0 32px
692
+ }
693
+
694
+ .hero h1 {
695
+ font-size: 2rem
696
+ }
697
+
698
+ .stats-grid {
699
+ grid-template-columns: repeat(2, 1fr);
700
+ gap: 12px
701
+ }
702
+
703
+ .stat-card {
704
+ padding: 20px 16px
705
+ }
706
+
707
+ .stat-card .stat-value {
708
+ font-size: 1.8rem
709
+ }
710
+
711
+ .checks-grid {
712
+ grid-template-columns: 1fr
713
+ }
714
+
715
+ .owasp-grid {
716
+ grid-template-columns: 1fr
717
+ }
718
+
719
+ .chart-container {
720
+ flex-direction: column;
721
+ align-items: center
722
+ }
723
+
724
+ .table-controls input {
725
+ width: 100%
726
+ }
727
+ }
728
+
729
+ @media(max-width:480px) {
730
+ .stats-grid {
731
+ grid-template-columns: 1fr
732
+ }
733
+
734
+ .container {
735
+ padding: 0 16px
736
+ }
737
+ }
738
+ </style>
739
+ </head>
740
+
741
+ <body>
742
+ <div class="grid-bg"></div>
743
+
744
+ <div class="container">
745
+ <!-- ═══ HERO ═══ -->
746
+ <section class="hero fade-up">
747
+ <div class="hero-badge"><span class="dot"></span> Live Security Intelligence</div>
748
+ <h1><span class="shield">🛡️</span> guard-scanner</h1>
749
+ <p class="tagline">VirusTotal for AI Agent Skills — real-time threat detection across 32 categories, 352 patterns,
750
+ and 8 MCP security checks.</p>
751
+ <div class="badges">
752
+ <img src="https://img.shields.io/npm/v/@guava-parity/guard-scanner?color=cb3837&style=flat-square" alt="npm">
753
+ <img src="https://img.shields.io/badge/tests-63%2F63-00ff88?style=flat-square" alt="tests">
754
+ <img src="https://img.shields.io/badge/deps-0-38bdf8?style=flat-square" alt="deps">
755
+ <img src="https://img.shields.io/badge/OWASP_ASI-100%25-a78bfa?style=flat-square" alt="OWASP">
756
+ <img src="https://img.shields.io/npm/l/guard-scanner?color=64748b&style=flat-square" alt="license">
757
+ </div>
758
+ <div class="sub-stats">
759
+ <span>🏷️ Version <span class="num" id="versionBadge">13.0.0</span></span>
760
+ <span>📦 Categories <span class="num">32</span></span>
761
+ <span>⚡ Avg Scan <span class="num">0.016ms</span></span>
762
+ <span>🔌 IDEs <span class="num">8</span></span>
763
+ </div>
764
+ </section>
765
+
766
+ <!-- ═══ STATS ═══ -->
767
+ <section class="stats-grid" id="statsGrid">
768
+ <div class="glass stat-card fade-up">
769
+ <span class="stat-icon">📋</span>
770
+ <div class="stat-value" id="statTotal">—</div>
771
+ <div class="stat-label">Skills Scanned</div>
772
+ </div>
773
+ <div class="glass stat-card fade-up">
774
+ <span class="stat-icon">✅</span>
775
+ <div class="stat-value" id="statClean">—</div>
776
+ <div class="stat-label">Clean Rate</div>
777
+ </div>
778
+ <div class="glass stat-card amber fade-up">
779
+ <span class="stat-icon">⚠️</span>
780
+ <div class="stat-value" id="statFindings">—</div>
781
+ <div class="stat-label">Findings</div>
782
+ </div>
783
+ <div class="glass stat-card blue fade-up">
784
+ <span class="stat-icon">🕐</span>
785
+ <div class="stat-value" id="statDate">—</div>
786
+ <div class="stat-label">Last Scan</div>
787
+ </div>
788
+ </section>
789
+
790
+ <!-- ═══ THREAT CATEGORIES ═══ -->
791
+ <section id="chartSection" class="fade-up">
792
+ <div class="section-head">
793
+ <h2><span class="icon">📊</span> Threat Category Breakdown</h2>
794
+ <p>Distribution of detected threats across 32 categories</p>
795
+ </div>
796
+ <div class="chart-container glass" style="padding:32px">
797
+ <div class="donut-wrap">
798
+ <svg viewBox="0 0 120 120" id="donutChart"></svg>
799
+ <div class="center-label">
800
+ <span class="big" id="donutTotal">0</span>
801
+ <span class="sub">findings</span>
802
+ </div>
803
+ </div>
804
+ <div class="legend" id="chartLegend"></div>
805
+ </div>
806
+ </section>
807
+
808
+ <!-- ═══ MCP SECURITY CHECKS ═══ -->
809
+ <section class="fade-up">
810
+ <div class="section-head">
811
+ <h2><span class="icon">🔒</span> 8 MCP Security Checks</h2>
812
+ <p>Deep inspection of AI editor MCP configurations across 8 IDEs</p>
813
+ </div>
814
+ <div class="checks-grid" id="checksGrid"></div>
815
+ </section>
816
+
817
+ <!-- ═══ OWASP ASI ═══ -->
818
+ <section class="fade-up">
819
+ <div class="section-head">
820
+ <h2><span class="icon">🏛️</span> OWASP ASI01–10 Coverage</h2>
821
+ <p>100% coverage of the OWASP Agentic Security Initiative top 10 risks</p>
822
+ </div>
823
+ <div class="owasp-grid" id="owaspGrid"></div>
824
+ </section>
825
+
826
+ <!-- ═══ SKILL RESULTS TABLE ═══ -->
827
+ <section class="fade-up">
828
+ <div class="section-head">
829
+ <h2><span class="icon">🔍</span> Skill Scan Results</h2>
830
+ <p>Detailed findings per scanned AI agent skill</p>
831
+ </div>
832
+ <div class="table-controls">
833
+ <input type="text" id="searchInput" placeholder="🔎 Search skills...">
834
+ <button class="filter-btn active" data-filter="all">All</button>
835
+ <button class="filter-btn" data-filter="clean">✅ Clean</button>
836
+ <button class="filter-btn" data-filter="findings">⚠️ Findings</button>
837
+ <button class="filter-btn" data-filter="error">❌ Error</button>
838
+ </div>
839
+ <div class="glass table-wrap">
840
+ <table>
841
+ <thead>
842
+ <tr>
843
+ <th data-sort="name">Skill ↕</th>
844
+ <th data-sort="status">Status ↕</th>
845
+ <th data-sort="risk">Risk ↕</th>
846
+ <th data-sort="findings">Findings ↕</th>
847
+ <th>Time</th>
848
+ </tr>
849
+ </thead>
850
+ <tbody id="resultsBody"></tbody>
851
+ </table>
852
+ </div>
853
+ </section>
854
+
855
+ <!-- ═══ FOOTER ═══ -->
856
+ <footer class="footer">
857
+ <div class="footer-links">
858
+ <a href="https://github.com/koatora20/guard-scanner">GitHub</a>
859
+ <a href="https://www.npmjs.com/package/@guava-parity/guard-scanner">npm</a>
860
+ <a href="https://github.com/koatora20/dual-shield-paper">Research Paper</a>
861
+ </div>
862
+ <p>Built with <span class="heart">♥</span> by <a href="https://github.com/koatora20">Guava Parity Institute</a> —
863
+ dee & Guava 🍈</p>
864
+ </footer>
865
+ </div>
866
+
867
+ <script>
868
+ 'use strict';
869
+
870
+ // ── MCP Security Checks data ──
871
+ const MCP_CHECKS = [
872
+ { id: 1, name: 'SECRET_IN_ENV', desc: 'Detects hardcoded API keys, tokens, and passwords in MCP server environment variables', ref: 'Original' },
873
+ { id: 2, name: 'PATH_TRAVERSAL', desc: 'Catches ../ directory traversal patterns in command arguments', ref: 'CVE-2026-27735' },
874
+ { id: 3, name: 'SUSPICIOUS_FILE_URI', desc: 'Flags file:// URIs targeting sensitive paths like /etc/passwd or ~/.ssh', ref: 'Smithery.ai' },
875
+ { id: 4, name: 'COMMAND_INJECTION', desc: 'Detects shell metacharacters (;, |, &, $(), backticks) in arguments', ref: 'CVE-2025-54135' },
876
+ { id: 5, name: 'HOMOGLYPH_NAME', desc: 'Identifies non-ASCII lookalike characters in MCP server names', ref: 'Palo Alto Unit 42' },
877
+ { id: 6, name: 'PROTOTYPE_POLLUTION', desc: 'Pre-parse raw JSON scan for __proto__, constructor, prototype injection', ref: 'CVE-2026-29063' },
878
+ { id: 7, name: 'TOOL_SHADOWING', desc: 'Levenshtein distance ≤2 comparison against 17 known MCP server names', ref: 'Snyk Invariant Labs' },
879
+ { id: 8, name: 'SUSPICIOUS_URL', desc: 'Flags external https:// URLs embedded in MCP tool arguments', ref: 'postmark-mcp incident' },
880
+ ];
881
+
882
+ // ── OWASP ASI01-10 data ──
883
+ const OWASP_ASI = [
884
+ { code: 'ASI01', label: 'Prompt Injection' },
885
+ { code: 'ASI02', label: 'Unsafe Tool/Function Calls' },
886
+ { code: 'ASI03', label: 'Agent Identity Spoofing' },
887
+ { code: 'ASI04', label: 'Privilege Escalation' },
888
+ { code: 'ASI05', label: 'Memory Poisoning' },
889
+ { code: 'ASI06', label: 'Data/Secret Exfiltration' },
890
+ { code: 'ASI07', label: 'Supply Chain Attack' },
891
+ { code: 'ASI08', label: 'Cascading Hallucination' },
892
+ { code: 'ASI09', label: 'Repudiation / Audit Failure' },
893
+ { code: 'ASI10', label: 'Denial of Service' },
894
+ ];
895
+
896
+ // ── Chart colors ──
897
+ const CHART_COLORS = [
898
+ '#00ff88', '#38bdf8', '#a78bfa', '#ffb800', '#ff3b5c', '#f472b6',
899
+ '#2dd4bf', '#fb923c', '#818cf8', '#a3e635', '#e879f9', '#fbbf24',
900
+ '#22d3ee', '#f87171', '#34d399', '#c084fc', '#fcd34d', '#6ee7b7',
901
+ '#93c5fd', '#fca5a1', '#86efac', '#c4b5fd', '#fde68a',
902
+ ];
903
+
904
+ // ── Demo data (used when fetch fails) ──
905
+ const DEMO_DATA = {
906
+ meta: { scanDate: new Date().toISOString(), scannerVersion: '13.0.0', totalSkills: 104 },
907
+ summary: { clean: 85, withFindings: 12, errors: 7, totalFindings: 34, cleanPercentage: 82 },
908
+ categoryBreakdown: {
909
+ 'Prompt Injection': 8, 'Malicious Code': 6, 'Exfiltration': 5,
910
+ 'Memory Poisoning': 4, 'MCP Security': 3, 'Credential Handling': 3,
911
+ 'Obfuscation': 2, 'Trust Exploitation': 2, 'Persistence': 1,
912
+ },
913
+ results: Array.from({ length: 104 }, (_, i) => {
914
+ const statuses = ['clean', 'clean', 'clean', 'clean', 'clean', 'clean', 'clean', 'findings', 'error'];
915
+ const s = statuses[i % statuses.length];
916
+ return {
917
+ name: `skill-${String(i + 1).padStart(3, '0')}`,
918
+ status: s,
919
+ findingsCount: s === 'findings' ? Math.ceil(Math.random() * 5) : 0,
920
+ riskScore: s === 'findings' ? +(Math.random() * 80 + 20).toFixed(1) : s === 'error' ? -1 : 0,
921
+ findings: [],
922
+ scannedAt: new Date(Date.now() - Math.random() * 3600000).toISOString(),
923
+ };
924
+ }),
925
+ };
926
+
927
+ // ── State ──
928
+ let DATA = null;
929
+ let sortCol = 'risk';
930
+ let sortDir = -1;
931
+ let filterStatus = 'all';
932
+
933
+ // ── Init ──
934
+ async function init() {
935
+ try {
936
+ // Try relative paths for both GitHub Pages and local dev (cache-bust)
937
+ const cb = `?_=${Date.now()}`;
938
+ let resp;
939
+ for (const url of [`data/latest.json${cb}`, `./data/latest.json${cb}`]) {
940
+ try { resp = await fetch(url); if (resp.ok) break; } catch { }
941
+ }
942
+ if (resp && resp.ok) {
943
+ DATA = await resp.json();
944
+ } else {
945
+ throw new Error('No data');
946
+ }
947
+ } catch {
948
+ DATA = DEMO_DATA;
949
+ console.log('ℹ️ Using demo data. Deploy data/latest.json for live results.');
950
+ }
951
+ renderAll();
952
+ }
953
+
954
+ function renderAll() {
955
+ renderStats();
956
+ renderChart();
957
+ renderChecks();
958
+ renderOwasp();
959
+ renderTable();
960
+ setupControls();
961
+ }
962
+
963
+ // ── Stats ──
964
+ function renderStats() {
965
+ const { summary, meta } = DATA;
966
+ animateValue('statTotal', summary.clean + summary.withFindings + summary.errors, '', 0);
967
+ animateValue('statClean', summary.cleanPercentage, '%', 0);
968
+ animateValue('statFindings', summary.totalFindings, '', 0);
969
+ const d = new Date(meta.scanDate);
970
+ document.getElementById('statDate').textContent = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
971
+ document.getElementById('versionBadge').textContent = meta.scannerVersion || '13.0.0';
972
+ }
973
+
974
+ function animateValue(id, target, suffix, decimals) {
975
+ const el = document.getElementById(id);
976
+ const dur = 1200;
977
+ const start = performance.now();
978
+ function tick(now) {
979
+ const t = Math.min((now - start) / dur, 1);
980
+ const ease = 1 - Math.pow(1 - t, 3);
981
+ const val = (target * ease).toFixed(decimals);
982
+ el.textContent = val + suffix;
983
+ if (t < 1) requestAnimationFrame(tick);
984
+ }
985
+ requestAnimationFrame(tick);
986
+ }
987
+
988
+ // ── Donut Chart ──
989
+ function renderChart() {
990
+ const cats = DATA.categoryBreakdown || {};
991
+ const entries = Object.entries(cats).sort((a, b) => b[1] - a[1]);
992
+ const total = entries.reduce((s, [, v]) => s + v, 0);
993
+ document.getElementById('donutTotal').textContent = total;
994
+
995
+ if (total === 0) {
996
+ document.getElementById('chartSection').querySelector('.chart-container').innerHTML =
997
+ '<div style="text-align:center;padding:40px;color:var(--green)"><div style="font-size:48px;margin-bottom:12px">✅</div><div style="font-size:18px;font-weight:700">All Clear</div><div style="color:var(--text-muted);font-size:14px;margin-top:4px">No threats detected in this scan</div></div>';
998
+ return;
999
+ }
1000
+
1001
+ const svg = document.getElementById('donutChart');
1002
+ const legend = document.getElementById('chartLegend');
1003
+ const R = 50, CX = 60, CY = 60, SW = 14;
1004
+ const C = 2 * Math.PI * R;
1005
+ let offset = 0;
1006
+ let html = '';
1007
+ let legendHtml = '';
1008
+
1009
+ entries.forEach(([cat, count], i) => {
1010
+ const pct = count / total;
1011
+ const len = pct * C;
1012
+ const color = CHART_COLORS[i % CHART_COLORS.length];
1013
+ html += `<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="${color}" stroke-width="${SW}" stroke-dasharray="${len} ${C - len}" stroke-dashoffset="${-offset}" opacity="0.9"/>`;
1014
+ offset += len;
1015
+ legendHtml += `<div class="legend-item"><span class="swatch" style="background:${color}"></span><span>${cat}</span><span class="count">${count}</span></div>`;
1016
+ });
1017
+
1018
+ svg.innerHTML = html;
1019
+ legend.innerHTML = legendHtml;
1020
+ }
1021
+
1022
+ // ── MCP Checks ──
1023
+ function renderChecks() {
1024
+ const grid = document.getElementById('checksGrid');
1025
+ grid.innerHTML = MCP_CHECKS.map(c =>
1026
+ `<div class="glass check-card">
1027
+ <div class="check-num">${c.id}</div>
1028
+ <div class="check-body">
1029
+ <h3>${c.name}</h3>
1030
+ <p>${c.desc}</p>
1031
+ <div class="ref">${c.ref}</div>
1032
+ </div>
1033
+ </div>`
1034
+ ).join('');
1035
+ }
1036
+
1037
+ // ── OWASP ASI ──
1038
+ function renderOwasp() {
1039
+ const grid = document.getElementById('owaspGrid');
1040
+ grid.innerHTML = OWASP_ASI.map(o =>
1041
+ `<div class="glass owasp-item">
1042
+ <span class="owasp-code">${o.code}</span>
1043
+ <span class="owasp-label">${o.label}</span>
1044
+ <span class="owasp-check">✓</span>
1045
+ </div>`
1046
+ ).join('');
1047
+ }
1048
+
1049
+ // ── Results Table ──
1050
+ function renderTable() {
1051
+ const body = document.getElementById('resultsBody');
1052
+ const search = (document.getElementById('searchInput')?.value || '').toLowerCase();
1053
+
1054
+ let rows = (DATA.results || []).filter(r => {
1055
+ if (filterStatus !== 'all' && r.status !== filterStatus) return false;
1056
+ if (search && !r.name.toLowerCase().includes(search)) return false;
1057
+ return true;
1058
+ });
1059
+
1060
+ rows.sort((a, b) => {
1061
+ let va, vb;
1062
+ switch (sortCol) {
1063
+ case 'name': va = a.name; vb = b.name; return sortDir * va.localeCompare(vb);
1064
+ case 'status': va = a.status; vb = b.status; return sortDir * va.localeCompare(vb);
1065
+ case 'risk': va = a.riskScore; vb = b.riskScore; return sortDir * (va - vb);
1066
+ case 'findings': va = a.findingsCount; vb = b.findingsCount; return sortDir * (va - vb);
1067
+ default: return 0;
1068
+ }
1069
+ });
1070
+
1071
+ body.innerHTML = rows.map(r => {
1072
+ const badgeClass = r.status === 'clean' ? 'badge-clean' : r.status === 'findings' ? 'badge-findings' : 'badge-error';
1073
+ const riskPct = Math.max(0, Math.min(100, r.riskScore));
1074
+ const riskColor = riskPct <= 30 ? 'risk-low' : riskPct <= 60 ? 'risk-med' : 'risk-high';
1075
+ const time = r.scannedAt ? new Date(r.scannedAt).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '—';
1076
+
1077
+ return `<tr>
1078
+ <td class="skill-name">${escHtml(r.name)}</td>
1079
+ <td><span class="badge ${badgeClass}">${r.status}</span></td>
1080
+ <td>
1081
+ <span class="risk-bar"><span class="risk-bar-fill ${riskColor}" style="width:${riskPct}%"></span></span>
1082
+ <span style="font-family:var(--mono);font-size:13px;color:var(--text-dim)">${r.riskScore >= 0 ? r.riskScore.toFixed(1) : '—'}</span>
1083
+ </td>
1084
+ <td style="font-family:var(--mono)">${r.findingsCount}</td>
1085
+ <td style="color:var(--text-muted);font-size:12px">${time}</td>
1086
+ </tr>`;
1087
+ }).join('');
1088
+ }
1089
+
1090
+ function escHtml(s) { return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
1091
+
1092
+ // ── Controls ──
1093
+ function setupControls() {
1094
+ document.getElementById('searchInput').addEventListener('input', renderTable);
1095
+
1096
+ document.querySelectorAll('.filter-btn').forEach(btn => {
1097
+ btn.addEventListener('click', () => {
1098
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
1099
+ btn.classList.add('active');
1100
+ filterStatus = btn.dataset.filter;
1101
+ renderTable();
1102
+ });
1103
+ });
1104
+
1105
+ document.querySelectorAll('th[data-sort]').forEach(th => {
1106
+ th.addEventListener('click', () => {
1107
+ const col = th.dataset.sort;
1108
+ if (sortCol === col) sortDir *= -1; else { sortCol = col; sortDir = -1; }
1109
+ renderTable();
1110
+ });
1111
+ });
1112
+ }
1113
+
1114
+ // ── Launch ──
1115
+ document.addEventListener('DOMContentLoaded', init);
1116
+ </script>
1117
+ </body>
1118
+
1119
+ </html>