@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.
- package/README.md +244 -195
- package/SKILL.md +13 -2
- package/demo/data/insaider-threat-model.tc.json +7800 -0
- package/demo/data/openclaw-demo.tc.json +80 -303
- package/demo/data/openclaw-nemoclaw-demo.tc.json +114 -389
- package/dist/aivss/scoring/heuristic-scorer.d.ts +1 -1
- package/dist/aivss/scoring/heuristic-scorer.js +1 -1
- package/dist/cli.js +24 -37
- package/dist/cli.js.map +1 -1
- package/dist/examples/insaider-assessment.d.ts +28 -0
- package/dist/examples/insaider-assessment.d.ts.map +1 -0
- package/dist/examples/insaider-assessment.js +301 -0
- package/dist/examples/insaider-assessment.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -9
- package/dist/index.js.map +1 -1
- package/dist/output/tc-assembler.d.ts +0 -10
- package/dist/output/tc-assembler.d.ts.map +1 -1
- package/dist/output/tc-assembler.js +4 -42
- package/dist/output/tc-assembler.js.map +1 -1
- package/dist/references/container-taxonomy.d.ts +41 -0
- package/dist/references/container-taxonomy.d.ts.map +1 -0
- package/dist/references/container-taxonomy.js +207 -0
- package/dist/references/container-taxonomy.js.map +1 -0
- package/dist/references/index.d.ts +0 -7
- package/dist/references/index.d.ts.map +1 -1
- package/dist/references/index.js +0 -13
- package/dist/references/index.js.map +1 -1
- package/dist/references/nist-ai-rmf.d.ts +1 -6
- package/dist/references/nist-ai-rmf.d.ts.map +1 -1
- package/dist/references/nist-ai-rmf.js +1 -10
- package/dist/references/nist-ai-rmf.js.map +1 -1
- package/dist/references/owasp-agentic-top10.d.ts +1 -6
- package/dist/references/owasp-agentic-top10.d.ts.map +1 -1
- package/dist/references/owasp-agentic-top10.js +0 -9
- package/dist/references/owasp-agentic-top10.js.map +1 -1
- package/package.json +4 -18
- package/demo/index.html +0 -556
- package/dist/aivss/scoring/a2a-corpus.test.d.ts +0 -28
- package/dist/aivss/scoring/a2a-corpus.test.d.ts.map +0 -1
- package/dist/aivss/scoring/a2a-corpus.test.js +0 -109
- package/dist/aivss/scoring/a2a-corpus.test.js.map +0 -1
- package/dist/aivss/scoring/determinism.test.d.ts +0 -16
- package/dist/aivss/scoring/determinism.test.d.ts.map +0 -1
- package/dist/aivss/scoring/determinism.test.js +0 -169
- package/dist/aivss/scoring/determinism.test.js.map +0 -1
- package/dist/aivss/scoring/scoring-mode.d.ts +0 -48
- package/dist/aivss/scoring/scoring-mode.d.ts.map +0 -1
- package/dist/aivss/scoring/scoring-mode.js +0 -64
- package/dist/aivss/scoring/scoring-mode.js.map +0 -1
- package/dist/references/registry-parity.test.d.ts +0 -17
- package/dist/references/registry-parity.test.d.ts.map +0 -1
- package/dist/references/registry-parity.test.js +0 -87
- 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"}
|