@waratahlabs/canopy 0.5.0 → 0.6.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 (54) hide show
  1. package/README.md +244 -195
  2. package/SKILL.md +13 -2
  3. package/demo/data/insaider-threat-model.tc.json +7800 -0
  4. package/demo/data/openclaw-demo.tc.json +80 -303
  5. package/demo/data/openclaw-nemoclaw-demo.tc.json +114 -389
  6. package/dist/aivss/scoring/heuristic-scorer.d.ts +1 -1
  7. package/dist/aivss/scoring/heuristic-scorer.js +1 -1
  8. package/dist/cli.js +24 -37
  9. package/dist/cli.js.map +1 -1
  10. package/dist/examples/insaider-assessment.d.ts +28 -0
  11. package/dist/examples/insaider-assessment.d.ts.map +1 -0
  12. package/dist/examples/insaider-assessment.js +301 -0
  13. package/dist/examples/insaider-assessment.js.map +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +0 -9
  16. package/dist/index.js.map +1 -1
  17. package/dist/output/tc-assembler.d.ts +0 -10
  18. package/dist/output/tc-assembler.d.ts.map +1 -1
  19. package/dist/output/tc-assembler.js +4 -42
  20. package/dist/output/tc-assembler.js.map +1 -1
  21. package/dist/references/container-taxonomy.d.ts +41 -0
  22. package/dist/references/container-taxonomy.d.ts.map +1 -0
  23. package/dist/references/container-taxonomy.js +207 -0
  24. package/dist/references/container-taxonomy.js.map +1 -0
  25. package/dist/references/index.d.ts +0 -7
  26. package/dist/references/index.d.ts.map +1 -1
  27. package/dist/references/index.js +0 -13
  28. package/dist/references/index.js.map +1 -1
  29. package/dist/references/nist-ai-rmf.d.ts +1 -6
  30. package/dist/references/nist-ai-rmf.d.ts.map +1 -1
  31. package/dist/references/nist-ai-rmf.js +1 -10
  32. package/dist/references/nist-ai-rmf.js.map +1 -1
  33. package/dist/references/owasp-agentic-top10.d.ts +1 -6
  34. package/dist/references/owasp-agentic-top10.d.ts.map +1 -1
  35. package/dist/references/owasp-agentic-top10.js +0 -9
  36. package/dist/references/owasp-agentic-top10.js.map +1 -1
  37. package/package.json +4 -18
  38. package/demo/index.html +0 -556
  39. package/dist/aivss/scoring/a2a-corpus.test.d.ts +0 -28
  40. package/dist/aivss/scoring/a2a-corpus.test.d.ts.map +0 -1
  41. package/dist/aivss/scoring/a2a-corpus.test.js +0 -109
  42. package/dist/aivss/scoring/a2a-corpus.test.js.map +0 -1
  43. package/dist/aivss/scoring/determinism.test.d.ts +0 -16
  44. package/dist/aivss/scoring/determinism.test.d.ts.map +0 -1
  45. package/dist/aivss/scoring/determinism.test.js +0 -169
  46. package/dist/aivss/scoring/determinism.test.js.map +0 -1
  47. package/dist/aivss/scoring/scoring-mode.d.ts +0 -48
  48. package/dist/aivss/scoring/scoring-mode.d.ts.map +0 -1
  49. package/dist/aivss/scoring/scoring-mode.js +0 -64
  50. package/dist/aivss/scoring/scoring-mode.js.map +0 -1
  51. package/dist/references/registry-parity.test.d.ts +0 -17
  52. package/dist/references/registry-parity.test.d.ts.map +0 -1
  53. package/dist/references/registry-parity.test.js +0 -87
  54. package/dist/references/registry-parity.test.js.map +0 -1
package/demo/index.html DELETED
@@ -1,556 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Canopy — AI Threat Model Explorer</title>
7
- <meta name="description" content="Interactive threat model for OpenClaw personal AI assistant. Generated by Canopy using OWASP AIVSS V4, MITRE ATLAS, and ASD/ACSC frameworks.">
8
- <style>
9
- :root {
10
- --bg: #0f0f0f;
11
- --surface: #1a1a1a;
12
- --border: #2a2a2a;
13
- --text: #e0e0e0;
14
- --muted: #888;
15
- --accent: #3b82f6;
16
- --critical: #ef4444;
17
- --high: #f97316;
18
- --medium: #eab308;
19
- --low: #22c55e;
20
- --none: #6b7280;
21
- --font-mono: "SF Mono", "Fira Code", "Cascadia Code", monospace;
22
- --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
23
- }
24
- * { box-sizing: border-box; margin: 0; padding: 0; }
25
- body { background: var(--bg); color: var(--text); font-family: var(--font-sans); font-size: 14px; line-height: 1.6; }
26
-
27
- header { border-bottom: 1px solid var(--border); padding: 20px 32px; display: flex; align-items: center; gap: 16px; }
28
- .logo { font-size: 20px; font-weight: 700; letter-spacing: -0.5px; color: #fff; }
29
- .logo span { color: var(--accent); }
30
- .tagline { color: var(--muted); font-size: 13px; }
31
- .header-meta { margin-left: auto; display: flex; gap: 12px; align-items: center; }
32
- .badge { background: var(--surface); border: 1px solid var(--border); border-radius: 4px; padding: 3px 8px; font-size: 11px; color: var(--muted); font-family: var(--font-mono); }
33
-
34
- /* View toggle */
35
- .view-toggle { display: flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; margin-bottom: 20px; }
36
- .view-tab { flex: 1; padding: 8px 16px; border: none; background: var(--surface); color: var(--muted); cursor: pointer; font-size: 12px; font-weight: 500; transition: background 0.15s, color 0.15s; text-align: center; }
37
- .view-tab:not(:last-child) { border-right: 1px solid var(--border); }
38
- .view-tab.active { background: #1e3a5f; color: #93c5fd; }
39
- .view-tab .tab-badge { display: inline-block; margin-left: 6px; background: #052040; border: 1px solid #1e3a5f; border-radius: 3px; padding: 0 5px; font-size: 10px; font-family: var(--font-mono); color: var(--muted); vertical-align: middle; }
40
- .view-tab.active .tab-badge { background: #0f2a4a; color: #60a5fa; }
41
-
42
- .layout { display: grid; grid-template-columns: 280px 1fr; min-height: calc(100vh - 61px); }
43
-
44
- aside { border-right: 1px solid var(--border); padding: 20px 0; }
45
- .sidebar-section { padding: 0 20px 16px; border-bottom: 1px solid var(--border); margin-bottom: 16px; }
46
- .sidebar-section:last-child { border-bottom: none; }
47
- .sidebar-label { font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; }
48
- .filter-btn { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; border-radius: 4px; border: none; background: transparent; color: var(--text); cursor: pointer; font-size: 13px; text-align: left; transition: background 0.1s; }
49
- .filter-btn:hover, .filter-btn.active { background: var(--surface); }
50
- .filter-btn .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
51
- .filter-btn .count { margin-left: auto; color: var(--muted); font-size: 11px; font-family: var(--font-mono); }
52
-
53
- main { padding: 24px 32px; }
54
- .system-info { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 16px 20px; margin-bottom: 24px; display: flex; gap: 32px; flex-wrap: wrap; }
55
- .info-item { }
56
- .info-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 2px; }
57
- .info-value { font-size: 13px; font-family: var(--font-mono); }
58
-
59
- .threat-list { display: flex; flex-direction: column; gap: 12px; }
60
- .threat-card { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; cursor: pointer; transition: border-color 0.15s; }
61
- .threat-card:hover { border-color: #444; }
62
- .threat-card.expanded { border-color: var(--accent); }
63
- .threat-header { padding: 14px 16px; display: flex; align-items: flex-start; gap: 12px; }
64
- .sev-pill { flex-shrink: 0; padding: 2px 8px; border-radius: 3px; font-size: 10px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; margin-top: 2px; }
65
- .sev-Critical { background: #7f1d1d; color: var(--critical); }
66
- .sev-High { background: #431407; color: var(--high); }
67
- .sev-Medium { background: #422006; color: var(--medium); }
68
- .sev-Low { background: #052e16; color: var(--low); }
69
- .threat-title { flex: 1; font-size: 14px; font-weight: 500; line-height: 1.4; }
70
- .threat-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; }
71
- .tag { background: #252525; border: 1px solid var(--border); border-radius: 3px; padding: 1px 6px; font-size: 10px; color: var(--muted); font-family: var(--font-mono); }
72
- .tag.atlas { color: #60a5fa; border-color: #1e3a5f; background: #0f2040; }
73
- .tag.metric { color: #a78bfa; border-color: #2e1f5e; background: #1a1030; }
74
- .tag.acsc { color: #34d399; border-color: #064e3b; background: #042f23; }
75
- .tag.mitigated { color: #4ade80; border-color: #14532d; background: #052e16; }
76
- .tag.residual { color: #f97316; border-color: #431407; background: #1f0d03; }
77
- .tag.partial { color: #eab308; border-color: #422006; background: #1a0f02; }
78
-
79
- /* Severity delta arrow */
80
- .sev-delta { display: flex; align-items: center; gap: 6px; flex-shrink: 0; margin-top: 2px; }
81
- .sev-delta .arrow { color: var(--muted); font-size: 12px; }
82
- .delta-reduced .arrow { color: #4ade80; }
83
- .delta-unchanged .arrow { color: var(--muted); }
84
-
85
- .threat-body { padding: 0 16px 16px; border-top: 1px solid var(--border); display: none; }
86
- .threat-card.expanded .threat-body { display: block; }
87
- .threat-section { margin-top: 14px; }
88
- .threat-section-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin-bottom: 6px; }
89
- .threat-section-content { font-size: 13px; color: #ccc; line-height: 1.6; }
90
-
91
- /* NeMo Claw control box */
92
- .nemo-control-box { margin-top: 14px; background: #0a1a0a; border: 1px solid #1a3a1a; border-radius: 4px; padding: 10px 14px; }
93
- .nemo-control-label { font-size: 10px; font-weight: 600; text-transform: uppercase; color: #4ade80; letter-spacing: 0.08em; margin-bottom: 4px; }
94
- .nemo-control-text { font-size: 12px; color: #a0c8a0; font-family: var(--font-mono); line-height: 1.5; }
95
- .nemo-control-residual { background: #1a0a00; border-color: #3a1800; }
96
- .nemo-control-residual .nemo-control-label { color: #f97316; }
97
- .nemo-control-residual .nemo-control-text { color: #c8a080; }
98
-
99
- .mitigation-list { margin-top: 24px; }
100
- .mitigation-card { background: #0f1f10; border: 1px solid #1a3a1a; border-radius: 6px; padding: 12px 16px; margin-bottom: 8px; }
101
- .mitigation-label { font-size: 10px; font-weight: 600; text-transform: uppercase; color: #4ade80; letter-spacing: 0.08em; margin-bottom: 6px; }
102
- .mitigation-text { font-size: 13px; color: #ccc; }
103
-
104
- /* Comparison summary banner */
105
- .comparison-banner { background: #0a1a0a; border: 1px solid #1a3a1a; border-radius: 6px; padding: 14px 20px; margin-bottom: 20px; display: flex; gap: 32px; flex-wrap: wrap; align-items: center; }
106
- .comparison-stat { text-align: center; }
107
- .comparison-stat .stat-num { font-size: 24px; font-weight: 700; font-family: var(--font-mono); }
108
- .comparison-stat .stat-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 2px; }
109
- .stat-reduced { color: #4ade80; }
110
- .stat-unchanged { color: var(--muted); }
111
- .stat-mitigated { color: #60a5fa; }
112
- .comparison-note { font-size: 12px; color: var(--muted); line-height: 1.6; max-width: 400px; }
113
-
114
- .empty { color: var(--muted); text-align: center; padding: 48px; }
115
-
116
- .framework-strip { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
117
- .fw-chip { display: flex; align-items: center; gap: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 4px; padding: 4px 10px; font-size: 11px; color: var(--muted); }
118
- .fw-dot { width: 6px; height: 6px; border-radius: 50%; }
119
-
120
- footer { border-top: 1px solid var(--border); padding: 16px 32px; display: flex; gap: 16px; align-items: center; }
121
- footer a { color: var(--muted); text-decoration: none; font-size: 12px; }
122
- footer a:hover { color: var(--text); }
123
- .footer-sep { color: var(--border); }
124
- </style>
125
- </head>
126
- <body>
127
-
128
- <header>
129
- <div>
130
- <div class="logo">🌿 <span>Canopy</span></div>
131
- <div class="tagline">Multi-framework AI threat modelling</div>
132
- </div>
133
- <div class="header-meta">
134
- <span class="badge">AIVSS V4</span>
135
- <span class="badge">ATLAS 5.6.0</span>
136
- <span class="badge">ASD/ACSC</span>
137
- </div>
138
- </header>
139
-
140
- <div class="layout">
141
- <aside>
142
- <div class="sidebar-section">
143
- <div class="sidebar-label">System</div>
144
- <button class="filter-btn active" data-filter="all">
145
- All threats
146
- <span class="count" id="count-all">—</span>
147
- </button>
148
- </div>
149
- <div class="sidebar-section">
150
- <div class="sidebar-label">Severity</div>
151
- <button class="filter-btn" data-filter="sev-Critical">
152
- <span class="dot" style="background:var(--critical)"></span> Critical
153
- <span class="count" id="count-Critical">0</span>
154
- </button>
155
- <button class="filter-btn" data-filter="sev-High">
156
- <span class="dot" style="background:var(--high)"></span> High
157
- <span class="count" id="count-High">0</span>
158
- </button>
159
- <button class="filter-btn" data-filter="sev-Medium">
160
- <span class="dot" style="background:var(--medium)"></span> Medium
161
- <span class="count" id="count-Medium">0</span>
162
- </button>
163
- <button class="filter-btn" data-filter="sev-Low">
164
- <span class="dot" style="background:var(--low)"></span> Low
165
- <span class="count" id="count-Low">0</span>
166
- </button>
167
- </div>
168
- <div class="sidebar-section">
169
- <div class="sidebar-label">AIVSS Metric</div>
170
- <div id="metric-filters"></div>
171
- </div>
172
- <!-- NeMo Claw filter panel (only shown in comparison view) -->
173
- <div class="sidebar-section" id="nemo-filter-section" style="display:none">
174
- <div class="sidebar-label">NeMo Claw</div>
175
- <button class="filter-btn" data-filter="nemo-mitigated">
176
- <span class="dot" style="background:var(--low)"></span> Mitigated
177
- <span class="count" id="count-mitigated">0</span>
178
- </button>
179
- <button class="filter-btn" data-filter="nemo-partial">
180
- <span class="dot" style="background:var(--medium)"></span> Partial
181
- <span class="count" id="count-partial">0</span>
182
- </button>
183
- <button class="filter-btn" data-filter="nemo-unchanged">
184
- <span class="dot" style="background:var(--muted)"></span> Unchanged
185
- <span class="count" id="count-unchanged">0</span>
186
- </button>
187
- </div>
188
- </aside>
189
-
190
- <main>
191
- <div class="view-toggle">
192
- <button class="view-tab active" id="tab-baseline" onclick="switchView('baseline')">
193
- OpenClaw — Baseline
194
- <span class="tab-badge" id="tab-baseline-count">—</span>
195
- </button>
196
- <button class="view-tab" id="tab-nemoclaw" onclick="switchView('nemoclaw')">
197
- OpenClaw + NeMo Claw
198
- <span class="tab-badge" id="tab-nemoclaw-count">—</span>
199
- </button>
200
- </div>
201
-
202
- <div class="framework-strip">
203
- <div class="fw-chip"><span class="fw-dot" style="background:#3b82f6"></span> OWASP AIVSS V4 — 9 metrics, 39 sub-categories</div>
204
- <div class="fw-chip"><span class="fw-dot" style="background:#a78bfa"></span> MITRE ATLAS 5.6.0 — adversary TTPs</div>
205
- <div class="fw-chip"><span class="fw-dot" style="background:#34d399"></span> ASD/ACSC Five Eyes</div>
206
- <div class="fw-chip" id="nemo-chip" style="display:none"><span class="fw-dot" style="background:#4ade80"></span> NeMo Claw — kernel-level sandbox (alpha)</div>
207
- </div>
208
-
209
- <!-- Comparison stats banner (only in NeMo view) -->
210
- <div class="comparison-banner" id="comparison-banner" style="display:none"></div>
211
-
212
- <div class="system-info" id="system-info"></div>
213
-
214
- <div class="threat-list" id="threat-list"></div>
215
-
216
- <div class="mitigation-list" id="mitigation-list"></div>
217
- </main>
218
- </div>
219
-
220
- <footer>
221
- <a href="https://github.com/waratahlabs/canopy">@waratahlabs/canopy</a>
222
- <span class="footer-sep">·</span>
223
- <a href="https://aivss.owasp.org">OWASP AIVSS</a>
224
- <span class="footer-sep">·</span>
225
- <a href="https://atlas.mitre.org">MITRE ATLAS</a>
226
- <span class="footer-sep">·</span>
227
- <a href="https://www.cyber.gov.au/business-government/secure-design/artificial-intelligence/careful-adoption-of-agentic-ai-services">ASD/ACSC</a>
228
- <span class="footer-sep">·</span>
229
- <a href="https://www.secondtalent.com/resources/nemoclaw-vs-openclaw/">NeMo Claw vs OpenClaw</a>
230
- <span class="footer-sep">·</span>
231
- <a href="https://github.com/awslabs/threat-composer">AWS Threat Composer</a>
232
- </footer>
233
-
234
- <script>
235
- const METRIC_NAMES = {
236
- MR: "Model Robustness",
237
- DS: "Data Security",
238
- EI: "Ethical Implications",
239
- DC: "Deployment Config",
240
- AD: "Agent Delegation",
241
- AA: "Agentic Autonomy",
242
- LL: "Lifecycle & Lineage",
243
- GV: "Governance",
244
- CS: "Supply Chain"
245
- };
246
-
247
- const SEV_ORDER = { Critical: 4, High: 3, Medium: 2, Low: 1, None: 0 };
248
-
249
- let currentView = "baseline";
250
- let baselineData = null;
251
- let nemoclawData = null;
252
- let activeFilter = "all";
253
-
254
- function getMeta(threat, key) {
255
- return threat.metadata?.find(m => m.key === key)?.value ?? "";
256
- }
257
-
258
- function activeData() {
259
- return currentView === "baseline" ? baselineData : nemoclawData;
260
- }
261
-
262
- function renderSystemInfo(data) {
263
- const el = document.getElementById("system-info");
264
- const appName = data.applicationInfo?.name ?? "Unknown";
265
- const threatCount = data.threats?.length ?? 0;
266
- const mitigationCount = data.mitigations?.length ?? 0;
267
- const version = data.metadata?.find(m => m.key === "canopy-version")?.value ?? "—";
268
- const aivss = data.metadata?.find(m => m.key === "aivss-version")?.value ?? "—";
269
- const generatedAt = data.metadata?.find(m => m.key === "generated-at")?.value ?? "";
270
- const date = generatedAt ? new Date(generatedAt).toLocaleDateString("en-AU", { dateStyle: "medium" }) : "—";
271
- const isolationLayers = data.metadata?.find(m => m.key === "isolation-layers")?.value ?? null;
272
-
273
- el.innerHTML = `
274
- <div class="info-item"><div class="info-label">System</div><div class="info-value">${appName}</div></div>
275
- <div class="info-item"><div class="info-label">Threats</div><div class="info-value">${threatCount}</div></div>
276
- <div class="info-item"><div class="info-label">Mitigations</div><div class="info-value">${mitigationCount}</div></div>
277
- <div class="info-item"><div class="info-label">Canopy</div><div class="info-value">v${version}</div></div>
278
- <div class="info-item"><div class="info-label">AIVSS</div><div class="info-value">v${aivss}</div></div>
279
- <div class="info-item"><div class="info-label">Generated</div><div class="info-value">${date}</div></div>
280
- ${isolationLayers ? `<div class="info-item"><div class="info-label">Isolation</div><div class="info-value" style="color:#4ade80">${isolationLayers}</div></div>` : ""}
281
- `;
282
- }
283
-
284
- function buildSevPill(sev) {
285
- return `<span class="sev-pill sev-${sev}">${sev}</span>`;
286
- }
287
-
288
- function buildSevDelta(baseline, current) {
289
- const bOrd = SEV_ORDER[baseline] ?? 2;
290
- const cOrd = SEV_ORDER[current] ?? 2;
291
- const deltaClass = cOrd < bOrd ? "delta-reduced" : "delta-unchanged";
292
- const arrow = cOrd < bOrd ? "↓" : "→";
293
- return `
294
- <div class="sev-delta ${deltaClass}">
295
- ${buildSevPill(baseline)}
296
- <span class="arrow">${arrow}</span>
297
- ${buildSevPill(current)}
298
- </div>
299
- `;
300
- }
301
-
302
- function renderThreat(t) {
303
- const sev = getMeta(t, "aivss-severity") || "Medium";
304
- const metric = getMeta(t, "aivss-metric");
305
- const atlasId = getMeta(t, "atlas-techniqueId");
306
- const atlasName = getMeta(t, "atlas-techniqueName");
307
- const tactic = getMeta(t, "atlas-tactic");
308
- const acscFamily = getMeta(t, "acsc-riskFamily");
309
-
310
- // NeMo Claw fields
311
- const nemoMitigated = getMeta(t, "nemoclaw-mitigated");
312
- const nemoControl = getMeta(t, "nemoclaw-control");
313
- const baselineSev = getMeta(t, "baseline-severity");
314
- const severityDelta = getMeta(t, "severity-delta");
315
- const isNemoView = currentView === "nemoclaw";
316
-
317
- // NeMo Claw status tag
318
- let nemoTag = "";
319
- if (isNemoView && nemoMitigated) {
320
- if (nemoMitigated === "true") nemoTag = `<span class="tag mitigated">NeMo: mitigated</span>`;
321
- else if (nemoMitigated === "partial") nemoTag = `<span class="tag partial">NeMo: partial</span>`;
322
- else if (nemoMitigated === "false") nemoTag = `<span class="tag residual">NeMo: residual risk</span>`;
323
- }
324
-
325
- const tags = [
326
- metric ? `<span class="tag metric">${metric} — ${METRIC_NAMES[metric] ?? metric}</span>` : "",
327
- atlasId ? `<span class="tag atlas">${atlasId} ${atlasName}</span>` : "",
328
- tactic ? `<span class="tag">${tactic}</span>` : "",
329
- acscFamily ? `<span class="tag acsc">ACSC: ${acscFamily}</span>` : "",
330
- nemoTag,
331
- ].filter(Boolean).join("");
332
-
333
- // Header: baseline view shows simple pill; NeMo view shows delta if severity changed
334
- const headerSev = (isNemoView && baselineSev && baselineSev !== sev)
335
- ? buildSevDelta(baselineSev, sev)
336
- : buildSevPill(sev);
337
-
338
- // NeMo control callout in body
339
- let nemoControlHtml = "";
340
- if (isNemoView && nemoControl && nemoControl !== "None — injection occurs inside sandboxed process") {
341
- const isResidual = nemoMitigated === "false";
342
- nemoControlHtml = `
343
- <div class="nemo-control-box ${isResidual ? 'nemo-control-residual' : ''}">
344
- <div class="nemo-control-label">${isResidual ? "Residual risk — NeMo Claw does not mitigate" : "NeMo Claw control"}</div>
345
- <div class="nemo-control-text">${nemoControl}</div>
346
- </div>
347
- `;
348
- } else if (isNemoView && nemoMitigated === "false") {
349
- nemoControlHtml = `
350
- <div class="nemo-control-box nemo-control-residual">
351
- <div class="nemo-control-label">Residual risk — NeMo Claw does not mitigate</div>
352
- <div class="nemo-control-text">${nemoControl || "This attack vector operates inside the sandbox boundary and is not addressed by kernel-level isolation."}</div>
353
- </div>
354
- `;
355
- }
356
-
357
- return `
358
- <div class="threat-card" data-id="${t.id}" data-sev="${sev}" data-metric="${metric}" data-nemo="${nemoMitigated}" onclick="toggleCard(this)">
359
- <div class="threat-header">
360
- ${headerSev}
361
- <div style="flex:1">
362
- <div class="threat-title">${t.threatAction ?? t.statement ?? "Unnamed threat"}</div>
363
- <div class="threat-tags">${tags}</div>
364
- </div>
365
- </div>
366
- <div class="threat-body">
367
- ${nemoControlHtml}
368
- ${t.threatSource ? `<div class="threat-section"><div class="threat-section-label">Threat source</div><div class="threat-section-content">${t.threatSource}</div></div>` : ""}
369
- ${t.prerequisites ? `<div class="threat-section"><div class="threat-section-label">Prerequisites</div><div class="threat-section-content">${t.prerequisites}</div></div>` : ""}
370
- ${t.threatImpact ? `<div class="threat-section"><div class="threat-section-label">Impact</div><div class="threat-section-content">${t.threatImpact}</div></div>` : ""}
371
- ${t.statement ? `<div class="threat-section"><div class="threat-section-label">Full statement</div><div class="threat-section-content">${t.statement}</div></div>` : ""}
372
- ${t.impactedAssets?.length ? `<div class="threat-section"><div class="threat-section-label">Impacted assets</div><div class="threat-tags">${t.impactedAssets.map(a => `<span class="tag">${a}</span>`).join("")}</div></div>` : ""}
373
- </div>
374
- </div>
375
- `;
376
- }
377
-
378
- function renderThreats(threats) {
379
- const list = document.getElementById("threat-list");
380
- if (!threats.length) {
381
- list.innerHTML = '<div class="empty">No threats match the current filter.</div>';
382
- return;
383
- }
384
- list.innerHTML = threats.map(renderThreat).join("");
385
- }
386
-
387
- function renderMitigations(mitigations) {
388
- const list = document.getElementById("mitigation-list");
389
- if (!mitigations.length) { list.innerHTML = ""; return; }
390
- list.innerHTML = `
391
- <div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:var(--muted);margin-bottom:12px;">Mitigations (${mitigations.length})</div>
392
- ${mitigations.map(m => `
393
- <div class="mitigation-card">
394
- <div class="mitigation-label">Mitigation ${m.numericId}</div>
395
- <div class="mitigation-text">${m.content}</div>
396
- </div>
397
- `).join("")}
398
- `;
399
- }
400
-
401
- function renderComparisonBanner(threats) {
402
- const banner = document.getElementById("comparison-banner");
403
- if (currentView !== "nemoclaw") { banner.style.display = "none"; return; }
404
-
405
- let reduced = 0, unchanged = 0, partial = 0, fullyMitigated = 0;
406
- for (const t of threats) {
407
- const m = getMeta(t, "nemoclaw-mitigated");
408
- const delta = getMeta(t, "severity-delta");
409
- if (m === "true") fullyMitigated++;
410
- else if (m === "partial") partial++;
411
- else unchanged++;
412
- if (delta === "reduced") reduced++;
413
- }
414
-
415
- banner.style.display = "flex";
416
- banner.innerHTML = `
417
- <div class="comparison-stat">
418
- <div class="stat-num stat-reduced">${reduced}</div>
419
- <div class="stat-label">Severity reduced</div>
420
- </div>
421
- <div class="comparison-stat">
422
- <div class="stat-num stat-unchanged">${unchanged}</div>
423
- <div class="stat-label">Unchanged</div>
424
- </div>
425
- <div class="comparison-stat">
426
- <div class="stat-num stat-mitigated">${fullyMitigated}</div>
427
- <div class="stat-label">Fully mitigated</div>
428
- </div>
429
- <div class="comparison-stat">
430
- <div class="stat-num" style="color:var(--medium)">${partial}</div>
431
- <div class="stat-label">Partially mitigated</div>
432
- </div>
433
- <div class="comparison-note">
434
- NeMo Claw's kernel-level sandbox (Landlock + seccomp + network namespaces) addresses host and engine layers
435
- directly. L1 Orchestration and L2 Application threats persist — the sandbox confines the blast radius but
436
- does not prevent injection or cron persistence.
437
- </div>
438
- `;
439
- }
440
-
441
- function renderFilters(threats) {
442
- const counts = { all: threats.length, Critical: 0, High: 0, Medium: 0, Low: 0, None: 0 };
443
- const metricCounts = {};
444
- const nemoCounts = { mitigated: 0, partial: 0, unchanged: 0 };
445
-
446
- for (const t of threats) {
447
- const sev = getMeta(t, "aivss-severity") || "Medium";
448
- if (sev in counts) counts[sev]++;
449
- const metric = getMeta(t, "aivss-metric");
450
- if (metric) metricCounts[metric] = (metricCounts[metric] ?? 0) + 1;
451
- const nemo = getMeta(t, "nemoclaw-mitigated");
452
- if (nemo === "true") nemoCounts.mitigated++;
453
- else if (nemo === "partial") nemoCounts.partial++;
454
- else if (nemo === "false") nemoCounts.unchanged++;
455
- }
456
-
457
- document.getElementById("count-all").textContent = counts.all;
458
- for (const sev of ["Critical", "High", "Medium", "Low"]) {
459
- document.getElementById(`count-${sev}`).textContent = counts[sev];
460
- }
461
- document.getElementById("count-mitigated").textContent = nemoCounts.mitigated;
462
- document.getElementById("count-partial").textContent = nemoCounts.partial;
463
- document.getElementById("count-unchanged").textContent = nemoCounts.unchanged;
464
-
465
- const mf = document.getElementById("metric-filters");
466
- mf.innerHTML = Object.entries(metricCounts).sort().map(([m, c]) => `
467
- <button class="filter-btn" data-filter="metric-${m}">
468
- <span style="font-family:var(--font-mono);font-size:11px;color:var(--accent)">${m}</span>
469
- ${METRIC_NAMES[m] ?? m}
470
- <span class="count">${c}</span>
471
- </button>
472
- `).join("");
473
- mf.querySelectorAll(".filter-btn").forEach(btn => btn.addEventListener("click", () => applyFilter(btn)));
474
-
475
- // Show/hide NeMo filter panel
476
- const nemoSection = document.getElementById("nemo-filter-section");
477
- nemoSection.style.display = currentView === "nemoclaw" ? "block" : "none";
478
- document.getElementById("nemo-chip").style.display = currentView === "nemoclaw" ? "flex" : "none";
479
- }
480
-
481
- function applyFilter(btn) {
482
- document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
483
- btn.classList.add("active");
484
- activeFilter = btn.dataset.filter;
485
- const data = activeData();
486
- if (!data) return;
487
- const filtered = filterThreats(data.threats ?? [], activeFilter);
488
- renderThreats(filtered);
489
- }
490
-
491
- function filterThreats(threats, filter) {
492
- if (filter === "all") return threats;
493
- if (filter.startsWith("sev-")) {
494
- const sev = filter.slice(4);
495
- return threats.filter(t => (getMeta(t, "aivss-severity") || "Medium") === sev);
496
- }
497
- if (filter.startsWith("metric-")) {
498
- const m = filter.slice(7);
499
- return threats.filter(t => getMeta(t, "aivss-metric") === m);
500
- }
501
- if (filter === "nemo-mitigated") return threats.filter(t => getMeta(t, "nemoclaw-mitigated") === "true");
502
- if (filter === "nemo-partial") return threats.filter(t => getMeta(t, "nemoclaw-mitigated") === "partial");
503
- if (filter === "nemo-unchanged") return threats.filter(t => getMeta(t, "nemoclaw-mitigated") === "false");
504
- return threats;
505
- }
506
-
507
- function toggleCard(el) {
508
- el.classList.toggle("expanded");
509
- }
510
-
511
- function switchView(view) {
512
- currentView = view;
513
- activeFilter = "all";
514
-
515
- document.getElementById("tab-baseline").classList.toggle("active", view === "baseline");
516
- document.getElementById("tab-nemoclaw").classList.toggle("active", view === "nemoclaw");
517
-
518
- const data = activeData();
519
- if (!data) return;
520
-
521
- const threats = data.threats ?? [];
522
- renderSystemInfo(data);
523
- renderFilters(threats);
524
- renderComparisonBanner(threats);
525
- renderThreats(threats);
526
- renderMitigations(data.mitigations ?? []);
527
-
528
- // Reset filter buttons to "all"
529
- document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
530
- document.querySelector(".filter-btn[data-filter='all']")?.classList.add("active");
531
- }
532
-
533
- document.querySelectorAll(".filter-btn[data-filter]").forEach(btn => {
534
- btn.addEventListener("click", () => applyFilter(btn));
535
- });
536
-
537
- // Load both datasets in parallel
538
- Promise.all([
539
- fetch("data/openclaw-demo.tc.json").then(r => r.json()),
540
- fetch("data/openclaw-nemoclaw-demo.tc.json").then(r => r.json()),
541
- ]).then(([baseline, nemoclaw]) => {
542
- baselineData = baseline;
543
- nemoclawData = nemoclaw;
544
-
545
- document.getElementById("tab-baseline-count").textContent = baseline.threats?.length ?? 0;
546
- document.getElementById("tab-nemoclaw-count").textContent = nemoclaw.threats?.length ?? 0;
547
-
548
- // Render baseline by default
549
- switchView("baseline");
550
- }).catch(err => {
551
- document.getElementById("threat-list").innerHTML =
552
- `<div class="empty">Failed to load demo data: ${err.message}</div>`;
553
- });
554
- </script>
555
- </body>
556
- </html>
@@ -1,28 +0,0 @@
1
- /**
2
- * A2A corpus regression — deterministic scoring against real agent definitions.
3
- *
4
- * The unit tests prove the scorer is pure over a hand-written fixture. This
5
- * proves it stays stable over a corpus of ~25 real agent topologies harvested
6
- * from a2aproject/a2a-samples, which is where scoring changes actually show up:
7
- * a threshold tweak that looks harmless against one synthetic agent will move
8
- * severities across a real corpus.
9
- *
10
- * Regenerate the corpus with a checkout of a2a-samples:
11
- *
12
- * bun scripts/harvest-a2a-samples.ts path/to/a2a-samples
13
- *
14
- * That writes tests/fixtures/a2a/input/<agent>.json (the synthesized
15
- * AgentDefinitions) and tests/fixtures/a2a/expected/<agent>.tc.json (the
16
- * assessed artifact). This test re-derives the expected output from the input
17
- * and diffs.
18
- *
19
- * A diff here is not automatically a bug. It means emitted scores moved, which
20
- * per CONTRIBUTING.md is a breaking change requiring a major version bump — so
21
- * the diff should be reviewed, then the corpus regenerated in the same commit
22
- * that changes the scoring.
23
- *
24
- * The suite skips cleanly when the corpus is absent, so a fresh clone without
25
- * fixtures is green rather than red.
26
- */
27
- export {};
28
- //# sourceMappingURL=a2a-corpus.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"a2a-corpus.test.d.ts","sourceRoot":"","sources":["../../../src/aivss/scoring/a2a-corpus.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG"}