@guava-parity/guard-scanner 13.0.0 → 16.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 (96) hide show
  1. package/README.md +170 -215
  2. package/README_ja.md +252 -0
  3. package/SECURITY.md +12 -4
  4. package/SKILL.md +148 -57
  5. package/dist/cli.cjs +5997 -0
  6. package/dist/cli.d.mts +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.mjs +6003 -0
  9. package/dist/index.cjs +4825 -0
  10. package/dist/index.d.mts +17 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.mjs +4798 -0
  13. package/dist/mcp-server.cjs +4756 -0
  14. package/dist/mcp-server.d.mts +1 -0
  15. package/dist/mcp-server.d.ts +1 -0
  16. package/dist/mcp-server.mjs +4767 -0
  17. package/dist/openclaw-plugin.cjs +4863 -0
  18. package/dist/openclaw-plugin.d.mts +11 -0
  19. package/dist/openclaw-plugin.d.ts +11 -0
  20. package/dist/openclaw-plugin.mjs +4854 -0
  21. package/dist/types.cjs +18 -0
  22. package/dist/types.d.mts +215 -0
  23. package/dist/types.d.ts +215 -0
  24. package/dist/types.mjs +1 -0
  25. package/docs/EVIDENCE_DRIVEN.md +182 -0
  26. package/docs/banner.png +0 -0
  27. package/docs/data/benchmark-ledger.json +1428 -0
  28. package/docs/data/corpus-metrics.json +11 -0
  29. package/docs/data/fp-ledger.json +18 -0
  30. package/docs/data/latest.json +25837 -2481
  31. package/docs/data/quality-contract.json +36 -0
  32. package/docs/generated/npm-audit-20260312.json +96 -0
  33. package/docs/generated/openclaw-upstream-status.json +25 -0
  34. package/docs/glossary.md +46 -0
  35. package/docs/index.html +1085 -496
  36. package/docs/logo.png +0 -0
  37. package/docs/openclaw-compatibility-audit.md +45 -0
  38. package/docs/openclaw-continuous-compatibility-plan.md +37 -0
  39. package/docs/rules/a2a-contagion.md +68 -0
  40. package/docs/rules/advanced-exfil.md +52 -0
  41. package/docs/rules/agent-protocol.md +108 -0
  42. package/docs/rules/api-abuse.md +68 -0
  43. package/docs/rules/autonomous-risk.md +92 -0
  44. package/docs/rules/config-impact.md +132 -0
  45. package/docs/rules/credential-handling.md +100 -0
  46. package/docs/rules/cve-patterns.md +332 -0
  47. package/docs/rules/data-exposure.md +84 -0
  48. package/docs/rules/exfiltration.md +36 -0
  49. package/docs/rules/financial-access.md +84 -0
  50. package/docs/rules/identity-hijack.md +140 -0
  51. package/docs/rules/inference-manipulation.md +60 -0
  52. package/docs/rules/leaky-skills.md +52 -0
  53. package/docs/rules/malicious-code.md +108 -0
  54. package/docs/rules/mcp-security.md +148 -0
  55. package/docs/rules/memory-poisoning.md +84 -0
  56. package/docs/rules/model-poisoning.md +44 -0
  57. package/docs/rules/obfuscation.md +60 -0
  58. package/docs/rules/persistence.md +108 -0
  59. package/docs/rules/pii-exposure.md +116 -0
  60. package/docs/rules/prompt-injection.md +148 -0
  61. package/docs/rules/prompt-worm.md +44 -0
  62. package/docs/rules/safeguard-bypass.md +44 -0
  63. package/docs/rules/sandbox-escape.md +100 -0
  64. package/docs/rules/secret-detection.md +44 -0
  65. package/docs/rules/supply-chain-v2.md +92 -0
  66. package/docs/rules/suspicious-download.md +60 -0
  67. package/docs/rules/trust-boundary.md +76 -0
  68. package/docs/rules/trust-exploitation.md +92 -0
  69. package/docs/rules/unverifiable-deps.md +84 -0
  70. package/docs/rules/vdb-injection.md +84 -0
  71. package/docs/security-vulnerability-report-20260312.md +53 -0
  72. package/docs/spec/PRD_V2_ARCHITECTURE.md +55 -0
  73. package/docs/spec/capabilities.json +174 -0
  74. package/docs/spec/finding.schema.json +104 -0
  75. package/docs/spec/integration-manifest.md +39 -0
  76. package/docs/spec/plugin-trust.json +11 -0
  77. package/docs/spec/sbom.json +33 -0
  78. package/docs/threat-model.md +65 -0
  79. package/docs/v13-architecture-manifest.md +55 -0
  80. package/hooks/context.ts +306 -0
  81. package/hooks/guard-scanner/plugin.ts +24 -1
  82. package/openclaw-plugin.mts +107 -0
  83. package/openclaw.plugin.json +30 -53
  84. package/package.json +66 -13
  85. package/src/asset-auditor.js +0 -508
  86. package/src/ci-reporter.js +0 -135
  87. package/src/cli.js +0 -294
  88. package/src/html-template.js +0 -239
  89. package/src/ioc-db.js +0 -54
  90. package/src/mcp-server.js +0 -702
  91. package/src/patterns.js +0 -611
  92. package/src/quarantine.js +0 -41
  93. package/src/runtime-guard.js +0 -346
  94. package/src/scanner.js +0 -1157
  95. package/src/vt-client.js +0 -202
  96. package/src/watcher.js +0 -170
package/docs/index.html CHANGED
@@ -1,441 +1,1029 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
+
3
4
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>guard-scanner — Security Dashboard</title>
7
- <meta name="description" content="Real-time security dashboard for AI agent skills. 352 patterns · 32 categories · 8 MCP checks · OWASP ASI01-10. Zero dependencies.">
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
11
- <style>
12
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
13
- :root{
14
- --bg:#0a0e17;--bg2:#111827;--surface:rgba(17,24,39,.65);
15
- --border:rgba(0,255,136,.12);--border-hover:rgba(0,255,136,.3);
16
- --green:#00ff88;--green-dim:rgba(0,255,136,.15);--green-glow:rgba(0,255,136,.25);
17
- --amber:#ffb800;--amber-dim:rgba(255,184,0,.15);
18
- --red:#ff3b5c;--red-dim:rgba(255,59,92,.15);
19
- --blue:#38bdf8;--blue-dim:rgba(56,189,248,.12);
20
- --purple:#a78bfa;--purple-dim:rgba(167,139,250,.12);
21
- --text:#e2e8f0;--text-dim:#94a3b8;--text-muted:#64748b;
22
- --font:'Inter',system-ui,sans-serif;--mono:'JetBrains Mono',monospace;
23
- --glass:saturate(180%) blur(20px);
24
- --radius:16px;--radius-sm:10px;
25
- }
26
- html{scroll-behavior:smooth}
27
- body{font-family:var(--font);background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden;line-height:1.6}
28
-
29
- /* ── Background Effects ── */
30
- body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;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%);z-index:0;pointer-events:none}
31
- .grid-bg{position:fixed;inset:0;background-image:linear-gradient(rgba(0,255,136,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,255,136,.03) 1px,transparent 1px);background-size:64px 64px;z-index:0;pointer-events:none;opacity:.5}
32
-
33
- /* ── Layout ── */
34
- .container{max-width:1200px;margin:0 auto;padding:0 24px;position:relative;z-index:1}
35
- section{margin-bottom:64px}
36
-
37
- /* ── Animations ── */
38
- @keyframes fadeUp{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}
39
- @keyframes pulse-glow{0%,100%{box-shadow:0 0 20px rgba(0,255,136,.1)}50%{box-shadow:0 0 40px rgba(0,255,136,.2)}}
40
- @keyframes count-up{from{opacity:.3}to{opacity:1}}
41
- .fade-up{animation:fadeUp .6s ease-out both}
42
- .fade-up:nth-child(2){animation-delay:.1s}
43
- .fade-up:nth-child(3){animation-delay:.2s}
44
- .fade-up:nth-child(4){animation-delay:.3s}
45
-
46
- /* ── Glass Card ── */
47
- .glass{background:var(--surface);backdrop-filter:var(--glass);-webkit-backdrop-filter:var(--glass);border:1px solid var(--border);border-radius:var(--radius);transition:border-color .3s,box-shadow .3s,transform .3s}
48
- .glass:hover{border-color:var(--border-hover);box-shadow:0 8px 32px rgba(0,255,136,.08);transform:translateY(-2px)}
49
-
50
- /* ── Hero ── */
51
- .hero{padding:80px 0 48px;text-align:center}
52
- .hero-badge{display:inline-flex;align-items:center;gap:8px;padding:6px 16px;border-radius:999px;background:var(--green-dim);border:1px solid rgba(0,255,136,.2);font-size:13px;font-weight:600;color:var(--green);letter-spacing:.03em;margin-bottom:24px}
53
- .hero-badge .dot{width:8px;height:8px;border-radius:50%;background:var(--green);animation:pulse-glow 2s ease-in-out infinite}
54
- .hero h1{font-size:clamp(2.5rem,6vw,4rem);font-weight:900;letter-spacing:-.03em;background:linear-gradient(135deg,#fff 0%,var(--green) 50%,var(--blue) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-bottom:12px}
55
- .hero h1 .shield{font-size:1em;-webkit-text-fill-color:var(--green);filter:drop-shadow(0 0 12px rgba(0,255,136,.4))}
56
- .hero .tagline{font-size:clamp(1.1rem,2.5vw,1.4rem);color:var(--text-dim);font-weight:500;max-width:640px;margin:0 auto 32px}
57
- .hero .badges{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}
58
- .hero .badges img{height:22px;border-radius:4px}
59
- .hero .sub-stats{display:flex;flex-wrap:wrap;gap:24px;justify-content:center;margin-top:24px;font-size:14px;color:var(--text-muted)}
60
- .hero .sub-stats span{display:flex;align-items:center;gap:6px}
61
- .hero .sub-stats .num{color:var(--green);font-family:var(--mono);font-weight:700}
62
-
63
- /* ── Stats Grid ── */
64
- .stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:20px;margin-bottom:48px}
65
- .stat-card{padding:28px 24px;text-align:center}
66
- .stat-card .stat-icon{font-size:28px;margin-bottom:12px;display:block}
67
- .stat-card .stat-value{font-family:var(--mono);font-size:2.4rem;font-weight:800;line-height:1;margin-bottom:6px;background:linear-gradient(135deg,#fff,var(--green));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
68
- .stat-card .stat-label{font-size:13px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;font-weight:600}
69
- .stat-card.amber .stat-value{background:linear-gradient(135deg,#fff,var(--amber));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
70
- .stat-card.red .stat-value{background:linear-gradient(135deg,#fff,var(--red));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
71
- .stat-card.blue .stat-value{background:linear-gradient(135deg,#fff,var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
72
-
73
- /* ── Section Headings ── */
74
- .section-head{margin-bottom:28px}
75
- .section-head h2{font-size:1.6rem;font-weight:800;letter-spacing:-.02em;display:flex;align-items:center;gap:10px}
76
- .section-head h2 .icon{font-size:1.2em}
77
- .section-head p{color:var(--text-dim);font-size:14px;margin-top:4px;max-width:600px}
78
-
79
- /* ── Checks Grid ── */
80
- .checks-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
81
- .check-card{padding:20px;display:flex;align-items:flex-start;gap:14px}
82
- .check-card .check-num{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--green);background:var(--green-dim);width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
83
- .check-card .check-body h3{font-size:14px;font-weight:700;margin-bottom:4px;color:#fff}
84
- .check-card .check-body p{font-size:12px;color:var(--text-muted);line-height:1.5}
85
- .check-card .check-body .ref{font-size:11px;color:var(--blue);font-family:var(--mono);margin-top:4px;opacity:.8}
86
-
87
- /* ── OWASP Grid ── */
88
- .owasp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
89
- .owasp-item{padding:16px;display:flex;align-items:center;gap:12px}
90
- .owasp-item .owasp-code{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--green);background:var(--green-dim);padding:4px 10px;border-radius:6px;white-space:nowrap}
91
- .owasp-item .owasp-label{font-size:13px;font-weight:500}
92
- .owasp-item .owasp-check{margin-left:auto;color:var(--green);font-size:16px;flex-shrink:0}
93
-
94
- /* ── Donut Chart ── */
95
- .chart-container{display:flex;flex-wrap:wrap;gap:40px;align-items:center;justify-content:center}
96
- .donut-wrap{position:relative;width:200px;height:200px;flex-shrink:0}
97
- .donut-wrap svg{width:100%;height:100%;transform:rotate(-90deg)}
98
- .donut-wrap .center-label{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
99
- .donut-wrap .center-label .big{font-family:var(--mono);font-size:2rem;font-weight:800;color:#fff}
100
- .donut-wrap .center-label .sub{font-size:12px;color:var(--text-muted);margin-top:2px}
101
- .legend{display:flex;flex-direction:column;gap:8px}
102
- .legend-item{display:flex;align-items:center;gap:10px;font-size:13px}
103
- .legend-item .swatch{width:12px;height:12px;border-radius:3px;flex-shrink:0}
104
- .legend-item .count{font-family:var(--mono);margin-left:auto;color:var(--text-dim);font-weight:600;min-width:24px;text-align:right}
105
-
106
- /* ── Skills Table ── */
107
- .table-wrap{overflow-x:auto;border-radius:var(--radius)}
108
- .table-controls{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:16px;align-items:center}
109
- .table-controls input{background:var(--surface);backdrop-filter:var(--glass);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 16px;color:var(--text);font-size:14px;font-family:var(--font);width:280px;outline:none;transition:border-color .3s}
110
- .table-controls input:focus{border-color:var(--green)}
111
- .table-controls input::placeholder{color:var(--text-muted)}
112
- .filter-btn{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px 16px;color:var(--text-dim);font-size:13px;font-weight:600;cursor:pointer;transition:all .2s;font-family:var(--font)}
113
- .filter-btn:hover,.filter-btn.active{border-color:var(--green);color:var(--green);background:var(--green-dim)}
114
- table{width:100%;border-collapse:collapse}
115
- table th{text-align:left;padding:14px 16px;font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);font-weight:700;border-bottom:1px solid var(--border);cursor:pointer;user-select:none;white-space:nowrap;transition:color .2s}
116
- table th:hover{color:var(--green)}
117
- table td{padding:12px 16px;font-size:14px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle}
118
- table tr{transition:background .2s}
119
- table tbody tr:hover{background:rgba(0,255,136,.03)}
120
- .skill-name{font-weight:600;font-family:var(--mono);font-size:13px}
121
- .badge{display:inline-block;padding:3px 10px;border-radius:6px;font-size:11px;font-weight:700;font-family:var(--mono);text-transform:uppercase;letter-spacing:.04em}
122
- .badge-clean{background:var(--green-dim);color:var(--green)}
123
- .badge-findings{background:var(--amber-dim);color:var(--amber)}
124
- .badge-error{background:var(--red-dim);color:var(--red)}
125
- .risk-bar{width:60px;height:6px;background:rgba(255,255,255,.08);border-radius:3px;overflow:hidden;display:inline-block;vertical-align:middle;margin-right:8px}
126
- .risk-bar-fill{height:100%;border-radius:3px;transition:width .4s ease}
127
- .risk-low{background:var(--green)}
128
- .risk-med{background:var(--amber)}
129
- .risk-high{background:var(--red)}
130
-
131
- /* ── Footer ── */
132
- .footer{text-align:center;padding:48px 0;border-top:1px solid var(--border);color:var(--text-muted);font-size:13px}
133
- .footer a{color:var(--green);text-decoration:none;font-weight:600;transition:opacity .2s}
134
- .footer a:hover{opacity:.7}
135
- .footer .footer-links{display:flex;flex-wrap:wrap;gap:24px;justify-content:center;margin-bottom:16px}
136
- .footer .heart{color:var(--red)}
137
-
138
- /* ── Responsive ── */
139
- @media(max-width:768px){
140
- .hero{padding:48px 0 32px}
141
- .hero h1{font-size:2rem}
142
- .stats-grid{grid-template-columns:repeat(2,1fr);gap:12px}
143
- .stat-card{padding:20px 16px}
144
- .stat-card .stat-value{font-size:1.8rem}
145
- .checks-grid{grid-template-columns:1fr}
146
- .owasp-grid{grid-template-columns:1fr}
147
- .chart-container{flex-direction:column;align-items:center}
148
- .table-controls input{width:100%}
149
- }
150
- @media(max-width:480px){
151
- .stats-grid{grid-template-columns:1fr}
152
- .container{padding:0 16px}
153
- }
154
- </style>
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>
155
739
  </head>
740
+
156
741
  <body>
157
- <div class="grid-bg"></div>
158
-
159
- <div class="container">
160
- <!-- ═══ HERO ═══ -->
161
- <section class="hero fade-up">
162
- <div class="hero-badge"><span class="dot"></span> Live Security Intelligence</div>
163
- <h1><span class="shield">🛡️</span> guard-scanner</h1>
164
- <p class="tagline">VirusTotal for AI Agent Skills — real-time threat detection across 32 categories, 352 patterns, and 8 MCP security checks.</p>
165
- <div class="badges">
166
- <img src="https://img.shields.io/npm/v/@guava-parity/guard-scanner?color=cb3837&style=flat-square" alt="npm">
167
- <img src="https://img.shields.io/badge/tests-63%2F63-00ff88?style=flat-square" alt="tests">
168
- <img src="https://img.shields.io/badge/deps-0-38bdf8?style=flat-square" alt="deps">
169
- <img src="https://img.shields.io/badge/OWASP_ASI-100%25-a78bfa?style=flat-square" alt="OWASP">
170
- <img src="https://img.shields.io/npm/l/guard-scanner?color=64748b&style=flat-square" alt="license">
171
- </div>
172
- <div class="sub-stats">
173
- <span>🏷️ Version <span class="num" id="versionBadge">13.0.0</span></span>
174
- <span>📦 Categories <span class="num">32</span></span>
175
- <span>⚡ Avg Scan <span class="num">0.016ms</span></span>
176
- <span>🔌 IDEs <span class="num">8</span></span>
177
- </div>
178
- </section>
179
-
180
- <!-- ═══ STATS ═══ -->
181
- <section class="stats-grid" id="statsGrid">
182
- <div class="glass stat-card fade-up">
183
- <span class="stat-icon">📋</span>
184
- <div class="stat-value" id="statTotal">—</div>
185
- <div class="stat-label">Skills Scanned</div>
186
- </div>
187
- <div class="glass stat-card fade-up">
188
- <span class="stat-icon">✅</span>
189
- <div class="stat-value" id="statClean">—</div>
190
- <div class="stat-label">Clean Rate</div>
191
- </div>
192
- <div class="glass stat-card amber fade-up">
193
- <span class="stat-icon">⚠️</span>
194
- <div class="stat-value" id="statFindings">—</div>
195
- <div class="stat-label">Findings</div>
196
- </div>
197
- <div class="glass stat-card blue fade-up">
198
- <span class="stat-icon">🕐</span>
199
- <div class="stat-value" id="statDate">—</div>
200
- <div class="stat-label">Last Scan</div>
201
- </div>
202
- </section>
203
-
204
- <!-- ═══ THREAT CATEGORIES ═══ -->
205
- <section id="chartSection" class="fade-up">
206
- <div class="section-head">
207
- <h2><span class="icon">📊</span> Threat Category Breakdown</h2>
208
- <p>Distribution of detected threats across 32 categories</p>
209
- </div>
210
- <div class="chart-container glass" style="padding:32px">
211
- <div class="donut-wrap">
212
- <svg viewBox="0 0 120 120" id="donutChart"></svg>
213
- <div class="center-label">
214
- <span class="big" id="donutTotal">0</span>
215
- <span class="sub">findings</span>
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>
216
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>
217
838
  </div>
218
- <div class="legend" id="chartLegend"></div>
219
- </div>
220
- </section>
221
-
222
- <!-- ═══ MCP SECURITY CHECKS ═══ -->
223
- <section class="fade-up">
224
- <div class="section-head">
225
- <h2><span class="icon">🔒</span> 8 MCP Security Checks</h2>
226
- <p>Deep inspection of AI editor MCP configurations across 8 IDEs</p>
227
- </div>
228
- <div class="checks-grid" id="checksGrid"></div>
229
- </section>
230
-
231
- <!-- ═══ OWASP ASI ═══ -->
232
- <section class="fade-up">
233
- <div class="section-head">
234
- <h2><span class="icon">🏛️</span> OWASP ASI01–10 Coverage</h2>
235
- <p>100% coverage of the OWASP Agentic Security Initiative top 10 risks</p>
236
- </div>
237
- <div class="owasp-grid" id="owaspGrid"></div>
238
- </section>
239
-
240
- <!-- ═══ SKILL RESULTS TABLE ═══ -->
241
- <section class="fade-up">
242
- <div class="section-head">
243
- <h2><span class="icon">🔍</span> Skill Scan Results</h2>
244
- <p>Detailed findings per scanned AI agent skill</p>
245
- </div>
246
- <div class="table-controls">
247
- <input type="text" id="searchInput" placeholder="🔎 Search skills...">
248
- <button class="filter-btn active" data-filter="all">All</button>
249
- <button class="filter-btn" data-filter="clean">✅ Clean</button>
250
- <button class="filter-btn" data-filter="findings">⚠️ Findings</button>
251
- <button class="filter-btn" data-filter="error">❌ Error</button>
252
- </div>
253
- <div class="glass table-wrap">
254
- <table>
255
- <thead>
256
- <tr>
257
- <th data-sort="name">Skill ↕</th>
258
- <th data-sort="status">Status ↕</th>
259
- <th data-sort="risk">Risk ↕</th>
260
- <th data-sort="findings">Findings ↕</th>
261
- <th>Time</th>
262
- </tr>
263
- </thead>
264
- <tbody id="resultsBody"></tbody>
265
- </table>
266
- </div>
267
- </section>
268
-
269
- <!-- ═══ FOOTER ═══ -->
270
- <footer class="footer">
271
- <div class="footer-links">
272
- <a href="https://github.com/koatora20/guard-scanner">GitHub</a>
273
- <a href="https://www.npmjs.com/package/@guava-parity/guard-scanner">npm</a>
274
- <a href="https://github.com/koatora20/dual-shield-paper">Research Paper</a>
275
- </div>
276
- <p>Built with <span class="heart">♥</span> by <a href="https://github.com/koatora20">Guava Parity Institute</a> — dee & Guava 🍈</p>
277
- </footer>
278
- </div>
279
-
280
- <script>
281
- 'use strict';
282
-
283
- // ── MCP Security Checks data ──
284
- const MCP_CHECKS = [
285
- { id: 1, name: 'SECRET_IN_ENV', desc: 'Detects hardcoded API keys, tokens, and passwords in MCP server environment variables', ref: 'Original' },
286
- { id: 2, name: 'PATH_TRAVERSAL', desc: 'Catches ../ directory traversal patterns in command arguments', ref: 'CVE-2026-27735' },
287
- { id: 3, name: 'SUSPICIOUS_FILE_URI', desc: 'Flags file:// URIs targeting sensitive paths like /etc/passwd or ~/.ssh', ref: 'Smithery.ai' },
288
- { id: 4, name: 'COMMAND_INJECTION', desc: 'Detects shell metacharacters (;, |, &, $(), backticks) in arguments', ref: 'CVE-2025-54135' },
289
- { id: 5, name: 'HOMOGLYPH_NAME', desc: 'Identifies non-ASCII lookalike characters in MCP server names', ref: 'Palo Alto Unit 42' },
290
- { id: 6, name: 'PROTOTYPE_POLLUTION', desc: 'Pre-parse raw JSON scan for __proto__, constructor, prototype injection', ref: 'CVE-2026-29063' },
291
- { id: 7, name: 'TOOL_SHADOWING', desc: 'Levenshtein distance ≤2 comparison against 17 known MCP server names', ref: 'Snyk Invariant Labs' },
292
- { id: 8, name: 'SUSPICIOUS_URL', desc: 'Flags external https:// URLs embedded in MCP tool arguments', ref: 'postmark-mcp incident' },
293
- ];
294
-
295
- // ── OWASP ASI01-10 data ──
296
- const OWASP_ASI = [
297
- { code: 'ASI01', label: 'Prompt Injection' },
298
- { code: 'ASI02', label: 'Unsafe Tool/Function Calls' },
299
- { code: 'ASI03', label: 'Agent Identity Spoofing' },
300
- { code: 'ASI04', label: 'Privilege Escalation' },
301
- { code: 'ASI05', label: 'Memory Poisoning' },
302
- { code: 'ASI06', label: 'Data/Secret Exfiltration' },
303
- { code: 'ASI07', label: 'Supply Chain Attack' },
304
- { code: 'ASI08', label: 'Cascading Hallucination' },
305
- { code: 'ASI09', label: 'Repudiation / Audit Failure' },
306
- { code: 'ASI10', label: 'Denial of Service' },
307
- ];
308
-
309
- // ── Chart colors ──
310
- const CHART_COLORS = [
311
- '#00ff88','#38bdf8','#a78bfa','#ffb800','#ff3b5c','#f472b6',
312
- '#2dd4bf','#fb923c','#818cf8','#a3e635','#e879f9','#fbbf24',
313
- '#22d3ee','#f87171','#34d399','#c084fc','#fcd34d','#6ee7b7',
314
- '#93c5fd','#fca5a1','#86efac','#c4b5fd','#fde68a',
315
- ];
316
-
317
- // ── Demo data (used when fetch fails) ──
318
- const DEMO_DATA = {
319
- meta: { scanDate: new Date().toISOString(), scannerVersion: '13.0.0', totalSkills: 104 },
320
- summary: { clean: 85, withFindings: 12, errors: 7, totalFindings: 34, cleanPercentage: 82 },
321
- categoryBreakdown: {
322
- 'Prompt Injection': 8, 'Malicious Code': 6, 'Exfiltration': 5,
323
- 'Memory Poisoning': 4, 'MCP Security': 3, 'Credential Handling': 3,
324
- 'Obfuscation': 2, 'Trust Exploitation': 2, 'Persistence': 1,
325
- },
326
- results: Array.from({ length: 104 }, (_, i) => {
327
- const statuses = ['clean','clean','clean','clean','clean','clean','clean','findings','error'];
328
- const s = statuses[i % statuses.length];
329
- return {
330
- name: `skill-${String(i + 1).padStart(3, '0')}`,
331
- status: s,
332
- findingsCount: s === 'findings' ? Math.ceil(Math.random() * 5) : 0,
333
- riskScore: s === 'findings' ? +(Math.random() * 80 + 20).toFixed(1) : s === 'error' ? -1 : 0,
334
- findings: [],
335
- scannedAt: new Date(Date.now() - Math.random() * 3600000).toISOString(),
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
+ }),
336
925
  };
337
- }),
338
- };
339
-
340
- // ── State ──
341
- let DATA = null;
342
- let sortCol = 'risk';
343
- let sortDir = -1;
344
- let filterStatus = 'all';
345
-
346
- // ── Init ──
347
- async function init() {
348
- try {
349
- // Try relative paths for both GitHub Pages and local dev
350
- let resp;
351
- for (const url of ['../data/latest.json', './data/latest.json', 'data/latest.json']) {
352
- try { resp = await fetch(url); if (resp.ok) break; } catch {}
353
- }
354
- if (resp && resp.ok) {
355
- DATA = await resp.json();
356
- } else {
357
- throw new Error('No data');
358
- }
359
- } catch {
360
- DATA = DEMO_DATA;
361
- console.log('ℹ️ Using demo data. Deploy data/latest.json for live results.');
362
- }
363
- renderAll();
364
- }
365
-
366
- function renderAll() {
367
- renderStats();
368
- renderChart();
369
- renderChecks();
370
- renderOwasp();
371
- renderTable();
372
- setupControls();
373
- }
374
-
375
- // ── Stats ──
376
- function renderStats() {
377
- const { summary, meta } = DATA;
378
- animateValue('statTotal', summary.clean + summary.withFindings + summary.errors, '', 0);
379
- animateValue('statClean', summary.cleanPercentage, '%', 0);
380
- animateValue('statFindings', summary.totalFindings, '', 0);
381
- const d = new Date(meta.scanDate);
382
- document.getElementById('statDate').textContent = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
383
- document.getElementById('versionBadge').textContent = meta.scannerVersion || '13.0.0';
384
- }
385
-
386
- function animateValue(id, target, suffix, decimals) {
387
- const el = document.getElementById(id);
388
- const dur = 1200;
389
- const start = performance.now();
390
- function tick(now) {
391
- const t = Math.min((now - start) / dur, 1);
392
- const ease = 1 - Math.pow(1 - t, 3);
393
- const val = (target * ease).toFixed(decimals);
394
- el.textContent = val + suffix;
395
- if (t < 1) requestAnimationFrame(tick);
396
- }
397
- requestAnimationFrame(tick);
398
- }
399
-
400
- // ── Donut Chart ──
401
- function renderChart() {
402
- const cats = DATA.categoryBreakdown || {};
403
- const entries = Object.entries(cats).sort((a, b) => b[1] - a[1]);
404
- const total = entries.reduce((s, [, v]) => s + v, 0);
405
- document.getElementById('donutTotal').textContent = total;
406
-
407
- if (total === 0) {
408
- document.getElementById('chartSection').querySelector('.chart-container').innerHTML =
409
- '<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>';
410
- return;
411
- }
412
-
413
- const svg = document.getElementById('donutChart');
414
- const legend = document.getElementById('chartLegend');
415
- const R = 50, CX = 60, CY = 60, SW = 14;
416
- const C = 2 * Math.PI * R;
417
- let offset = 0;
418
- let html = '';
419
- let legendHtml = '';
420
-
421
- entries.forEach(([cat, count], i) => {
422
- const pct = count / total;
423
- const len = pct * C;
424
- const color = CHART_COLORS[i % CHART_COLORS.length];
425
- 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"/>`;
426
- offset += len;
427
- legendHtml += `<div class="legend-item"><span class="swatch" style="background:${color}"></span><span>${cat}</span><span class="count">${count}</span></div>`;
428
- });
429
-
430
- svg.innerHTML = html;
431
- legend.innerHTML = legendHtml;
432
- }
433
-
434
- // ── MCP Checks ──
435
- function renderChecks() {
436
- const grid = document.getElementById('checksGrid');
437
- grid.innerHTML = MCP_CHECKS.map(c =>
438
- `<div class="glass check-card">
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">
439
1027
  <div class="check-num">${c.id}</div>
440
1028
  <div class="check-body">
441
1029
  <h3>${c.name}</h3>
@@ -443,50 +1031,50 @@ function renderChecks() {
443
1031
  <div class="ref">${c.ref}</div>
444
1032
  </div>
445
1033
  </div>`
446
- ).join('');
447
- }
448
-
449
- // ── OWASP ASI ──
450
- function renderOwasp() {
451
- const grid = document.getElementById('owaspGrid');
452
- grid.innerHTML = OWASP_ASI.map(o =>
453
- `<div class="glass owasp-item">
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">
454
1042
  <span class="owasp-code">${o.code}</span>
455
1043
  <span class="owasp-label">${o.label}</span>
456
1044
  <span class="owasp-check">✓</span>
457
1045
  </div>`
458
- ).join('');
459
- }
460
-
461
- // ── Results Table ──
462
- function renderTable() {
463
- const body = document.getElementById('resultsBody');
464
- const search = (document.getElementById('searchInput')?.value || '').toLowerCase();
465
-
466
- let rows = (DATA.results || []).filter(r => {
467
- if (filterStatus !== 'all' && r.status !== filterStatus) return false;
468
- if (search && !r.name.toLowerCase().includes(search)) return false;
469
- return true;
470
- });
471
-
472
- rows.sort((a, b) => {
473
- let va, vb;
474
- switch (sortCol) {
475
- case 'name': va = a.name; vb = b.name; return sortDir * va.localeCompare(vb);
476
- case 'status': va = a.status; vb = b.status; return sortDir * va.localeCompare(vb);
477
- case 'risk': va = a.riskScore; vb = b.riskScore; return sortDir * (va - vb);
478
- case 'findings': va = a.findingsCount; vb = b.findingsCount; return sortDir * (va - vb);
479
- default: return 0;
480
- }
481
- });
482
-
483
- body.innerHTML = rows.map(r => {
484
- const badgeClass = r.status === 'clean' ? 'badge-clean' : r.status === 'findings' ? 'badge-findings' : 'badge-error';
485
- const riskPct = Math.max(0, Math.min(100, r.riskScore));
486
- const riskColor = riskPct <= 30 ? 'risk-low' : riskPct <= 60 ? 'risk-med' : 'risk-high';
487
- const time = r.scannedAt ? new Date(r.scannedAt).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '—';
488
-
489
- return `<tr>
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>
490
1078
  <td class="skill-name">${escHtml(r.name)}</td>
491
1079
  <td><span class="badge ${badgeClass}">${r.status}</span></td>
492
1080
  <td>
@@ -496,35 +1084,36 @@ function renderTable() {
496
1084
  <td style="font-family:var(--mono)">${r.findingsCount}</td>
497
1085
  <td style="color:var(--text-muted);font-size:12px">${time}</td>
498
1086
  </tr>`;
499
- }).join('');
500
- }
1087
+ }).join('');
1088
+ }
501
1089
 
502
- function escHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
1090
+ function escHtml(s) { return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
503
1091
 
504
- // ── Controls ──
505
- function setupControls() {
506
- document.getElementById('searchInput').addEventListener('input', renderTable);
1092
+ // ── Controls ──
1093
+ function setupControls() {
1094
+ document.getElementById('searchInput').addEventListener('input', renderTable);
507
1095
 
508
- document.querySelectorAll('.filter-btn').forEach(btn => {
509
- btn.addEventListener('click', () => {
510
- document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
511
- btn.classList.add('active');
512
- filterStatus = btn.dataset.filter;
513
- renderTable();
514
- });
515
- });
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
+ });
516
1104
 
517
- document.querySelectorAll('th[data-sort]').forEach(th => {
518
- th.addEventListener('click', () => {
519
- const col = th.dataset.sort;
520
- if (sortCol === col) sortDir *= -1; else { sortCol = col; sortDir = -1; }
521
- renderTable();
522
- });
523
- });
524
- }
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
+ }
525
1113
 
526
- // ── Launch ──
527
- document.addEventListener('DOMContentLoaded', init);
528
- </script>
1114
+ // ── Launch ──
1115
+ document.addEventListener('DOMContentLoaded', init);
1116
+ </script>
529
1117
  </body>
530
- </html>
1118
+
1119
+ </html>