@denial-web/clawguard 0.1.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 (83) hide show
  1. package/.clawguard.example.json +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +241 -0
  4. package/SECURITY.md +33 -0
  5. package/action.yml +72 -0
  6. package/docs/ARCHITECTURE.md +312 -0
  7. package/docs/ARCHITECTURE_ROADMAP.md +267 -0
  8. package/docs/CLAWHUB_METADATA.md +57 -0
  9. package/docs/DEMO_CAPTURE.md +25 -0
  10. package/docs/DEMO_SCRIPT.md +87 -0
  11. package/docs/DEPENDENCY_SCANNING.md +61 -0
  12. package/docs/GITHUB_ACTION.md +56 -0
  13. package/docs/GITHUB_REPO_SETUP.md +76 -0
  14. package/docs/HTML_REPORTS.md +27 -0
  15. package/docs/INTEGRATION_SPEC.md +253 -0
  16. package/docs/LAUNCH_CHECKLIST.md +64 -0
  17. package/docs/LAUNCH_PLAN.md +40 -0
  18. package/docs/LOCAL_PROJECT_ASSETS.md +250 -0
  19. package/docs/MCP_PLUGIN_SCANNING.md +53 -0
  20. package/docs/NEXT_SESSION.md +110 -0
  21. package/docs/NPM_PUBLISHING.md +66 -0
  22. package/docs/OPENCLAW_CLAWHUB_RESEARCH.md +128 -0
  23. package/docs/POLICY_MODEL.md +198 -0
  24. package/docs/PROJECT_REVIEW.md +108 -0
  25. package/docs/REAL_WORLD_VALIDATION.md +57 -0
  26. package/docs/RELEASE_NOTES_v0.1.0.md +52 -0
  27. package/docs/REPORT_SCHEMA.md +81 -0
  28. package/docs/RULES.md +92 -0
  29. package/docs/THREAT_MODEL.md +50 -0
  30. package/docs/WEB_DEMO.md +39 -0
  31. package/docs/WORKSPACE_SCANNING.md +41 -0
  32. package/examples/clawhub-origin-without-lock/skills/orphan-helper/.clawhub/origin.json +6 -0
  33. package/examples/clawhub-origin-without-lock/skills/orphan-helper/SKILL.md +11 -0
  34. package/examples/clawhub-workspace/.clawhub/lock.json +22 -0
  35. package/examples/clawhub-workspace/skills/drift-helper/.clawhub/origin.json +6 -0
  36. package/examples/clawhub-workspace/skills/drift-helper/SKILL.md +11 -0
  37. package/examples/clawhub-workspace/skills/missing-origin/SKILL.md +11 -0
  38. package/examples/clawhub-workspace/skills/weather-helper/.clawhub/origin.json +6 -0
  39. package/examples/clawhub-workspace/skills/weather-helper/SKILL.md +15 -0
  40. package/examples/declared-api-skill/SKILL.md +27 -0
  41. package/examples/dependency-python-skill/SKILL.md +16 -0
  42. package/examples/dependency-python-skill/pyproject.toml +5 -0
  43. package/examples/dependency-python-skill/requirements.txt +3 -0
  44. package/examples/dependency-risky-skill/SKILL.md +16 -0
  45. package/examples/dependency-risky-skill/package.json +12 -0
  46. package/examples/dependency-safe-skill/SKILL.md +16 -0
  47. package/examples/dependency-safe-skill/package-lock.json +19 -0
  48. package/examples/dependency-safe-skill/package.json +7 -0
  49. package/examples/metadata-mismatch-skill/SKILL.md +22 -0
  50. package/examples/openclaw-plugin-config/.openclaw/plugins.json +18 -0
  51. package/examples/openclaw-workspace/.agents/skills/research-helper/SKILL.md +11 -0
  52. package/examples/openclaw-workspace/skills/notes/SKILL.md +3 -0
  53. package/examples/openclaw-workspace/skills/research-helper/SKILL.md +17 -0
  54. package/examples/risky-mcp-config/.cursor/mcp.json +29 -0
  55. package/examples/risky-openclaw-plugin/openclaw.plugin.json +6 -0
  56. package/examples/risky-openclaw-plugin/package.json +7 -0
  57. package/examples/risky-openclaw-plugin/src/index.ts +1 -0
  58. package/examples/risky-skill/SKILL.md +17 -0
  59. package/examples/safe-mcp-config/.cursor/mcp.json +15 -0
  60. package/examples/safe-openclaw-plugin/dist/index.js +1 -0
  61. package/examples/safe-openclaw-plugin/openclaw.plugin.json +5 -0
  62. package/examples/safe-openclaw-plugin/package.json +14 -0
  63. package/examples/safe-skill/SKILL.md +12 -0
  64. package/package.json +49 -0
  65. package/schemas/clawguard-report.schema.json +266 -0
  66. package/scripts/capture-demo.js +206 -0
  67. package/src/clawhub.js +383 -0
  68. package/src/cli.js +296 -0
  69. package/src/config.js +205 -0
  70. package/src/dependencies.js +417 -0
  71. package/src/mcp-config.js +592 -0
  72. package/src/policy.js +165 -0
  73. package/src/reporters/html.js +482 -0
  74. package/src/reporters/sarif.js +121 -0
  75. package/src/rule-catalog.js +400 -0
  76. package/src/rules.js +121 -0
  77. package/src/scanner.js +387 -0
  78. package/src/skill-metadata.js +516 -0
  79. package/src/web-server.js +395 -0
  80. package/src/workspace.js +233 -0
  81. package/web/app.js +374 -0
  82. package/web/index.html +119 -0
  83. package/web/styles.css +453 -0
package/web/app.js ADDED
@@ -0,0 +1,374 @@
1
+ const state = {
2
+ lastResult: null,
3
+ examples: []
4
+ };
5
+
6
+ const sampleSkill = `---
7
+ name: demo-weather-helper
8
+ description: Checks weather from an external API.
9
+ version: 0.1.0
10
+ author: ClawGuard Demo
11
+ category: productivity
12
+ metadata:
13
+ openclaw:
14
+ requires:
15
+ env:
16
+ - WEATHER_API_KEY
17
+ ---
18
+
19
+ # Demo Weather Helper
20
+
21
+ Use WEATHER_API_KEY to call https://api.weather.example/status.
22
+ Keep the request read-only and do not install dependencies.`;
23
+
24
+ const elements = {
25
+ policy: document.querySelector("#policy"),
26
+ input: document.querySelector("#skill-input"),
27
+ loadSample: document.querySelector("#load-sample"),
28
+ scanPaste: document.querySelector("#scan-paste"),
29
+ clearInput: document.querySelector("#clear-input"),
30
+ folderInput: document.querySelector("#folder-input"),
31
+ scanFolder: document.querySelector("#scan-folder"),
32
+ folderStatus: document.querySelector("#folder-status"),
33
+ examples: document.querySelector("#examples"),
34
+ targetName: document.querySelector("#target-name"),
35
+ sourcePill: document.querySelector("#source-pill"),
36
+ score: document.querySelector("#score"),
37
+ level: document.querySelector("#level"),
38
+ decision: document.querySelector("#decision"),
39
+ reason: document.querySelector("#reason"),
40
+ actions: document.querySelector("#actions"),
41
+ critical: document.querySelector("#critical-count"),
42
+ high: document.querySelector("#high-count"),
43
+ medium: document.querySelector("#medium-count"),
44
+ low: document.querySelector("#low-count"),
45
+ files: document.querySelector("#files-count"),
46
+ workspace: document.querySelector("#workspace-count"),
47
+ clawhub: document.querySelector("#clawhub-count"),
48
+ dependencies: document.querySelector("#dependency-count"),
49
+ findings: document.querySelector("#findings"),
50
+ downloadHtml: document.querySelector("#download-html"),
51
+ copyJson: document.querySelector("#copy-json")
52
+ };
53
+
54
+ init();
55
+
56
+ async function init() {
57
+ bindEvents();
58
+ await loadExamples();
59
+ }
60
+
61
+ function bindEvents() {
62
+ elements.loadSample.addEventListener("click", () => {
63
+ elements.input.value = sampleSkill;
64
+ });
65
+
66
+ elements.clearInput.addEventListener("click", () => {
67
+ elements.input.value = "";
68
+ elements.input.focus();
69
+ });
70
+
71
+ elements.scanPaste.addEventListener("click", async () => {
72
+ await scanPaste();
73
+ });
74
+
75
+ elements.folderInput.addEventListener("change", () => {
76
+ const files = [...elements.folderInput.files];
77
+ elements.scanFolder.disabled = files.length === 0;
78
+ elements.folderStatus.textContent = files.length === 0 ? "Choose a local folder to scan its files." : `${files.length} files selected`;
79
+ });
80
+
81
+ elements.scanFolder.addEventListener("click", async () => {
82
+ await scanFolder();
83
+ });
84
+
85
+ elements.copyJson.addEventListener("click", async () => {
86
+ if (!state.lastResult) return;
87
+ try {
88
+ await navigator.clipboard.writeText(JSON.stringify(state.lastResult.scan, null, 2));
89
+ setCopyButtonText("Copied");
90
+ } catch {
91
+ setCopyButtonText("Copy failed");
92
+ }
93
+ });
94
+
95
+ elements.downloadHtml.addEventListener("click", async () => {
96
+ if (!state.lastResult) return;
97
+ try {
98
+ const response = await fetch("/api/html-report", {
99
+ method: "POST",
100
+ headers: {
101
+ "content-type": "application/json"
102
+ },
103
+ body: JSON.stringify({
104
+ scan: state.lastResult.scan
105
+ })
106
+ });
107
+
108
+ if (!response.ok) {
109
+ const data = await response.json();
110
+ throw new Error(data.error ?? "Report export failed");
111
+ }
112
+
113
+ const html = await response.text();
114
+ downloadText(`${safeFilename(state.lastResult.displayTarget)}-clawguard.html`, html, "text/html");
115
+ setDownloadButtonText("Downloaded");
116
+ } catch {
117
+ setDownloadButtonText("Export failed");
118
+ }
119
+ });
120
+ }
121
+
122
+ async function loadExamples() {
123
+ try {
124
+ const data = await fetchJson("/api/examples");
125
+ state.examples = data.examples ?? [];
126
+ renderExamples();
127
+ } catch (error) {
128
+ elements.examples.innerHTML = `<div class="empty-state">${escapeHtml(error.message)}</div>`;
129
+ }
130
+ }
131
+
132
+ function renderExamples() {
133
+ elements.examples.innerHTML = state.examples.map((example) => `
134
+ <button class="example-card" type="button" data-example="${escapeHtml(example.id)}">
135
+ <strong>${escapeHtml(example.label)}</strong>
136
+ <p>${escapeHtml(example.description)}</p>
137
+ </button>
138
+ `).join("");
139
+
140
+ for (const button of elements.examples.querySelectorAll("[data-example]")) {
141
+ button.addEventListener("click", async () => {
142
+ await scanExample(button.dataset.example);
143
+ });
144
+ }
145
+ }
146
+
147
+ async function scanPaste() {
148
+ setBusy(true);
149
+ try {
150
+ const result = await fetchJson("/api/scan", {
151
+ method: "POST",
152
+ body: JSON.stringify({
153
+ text: elements.input.value,
154
+ filename: "SKILL.md",
155
+ policy: elements.policy.value
156
+ })
157
+ });
158
+ renderResult(result);
159
+ } catch (error) {
160
+ renderError(error);
161
+ } finally {
162
+ setBusy(false);
163
+ }
164
+ }
165
+
166
+ async function scanExample(example) {
167
+ setBusy(true);
168
+ try {
169
+ const result = await fetchJson("/api/scan-example", {
170
+ method: "POST",
171
+ body: JSON.stringify({
172
+ example,
173
+ policy: elements.policy.value
174
+ })
175
+ });
176
+ renderResult(result);
177
+ } catch (error) {
178
+ renderError(error);
179
+ } finally {
180
+ setBusy(false);
181
+ }
182
+ }
183
+
184
+ async function scanFolder() {
185
+ const selectedFiles = [...elements.folderInput.files];
186
+ if (selectedFiles.length === 0) {
187
+ renderError(new Error("Choose a folder before scanning."));
188
+ return;
189
+ }
190
+
191
+ setBusy(true);
192
+ try {
193
+ const files = await readSelectedFiles(selectedFiles);
194
+ const result = await fetchJson("/api/scan-files", {
195
+ method: "POST",
196
+ body: JSON.stringify({
197
+ files,
198
+ label: folderLabelFor(selectedFiles),
199
+ policy: elements.policy.value
200
+ })
201
+ });
202
+ renderResult(result);
203
+ } catch (error) {
204
+ renderError(error);
205
+ } finally {
206
+ setBusy(false);
207
+ }
208
+ }
209
+
210
+ function renderResult(result) {
211
+ state.lastResult = result;
212
+ const scan = result.scan;
213
+ const summary = scan.summary ?? {};
214
+ const policy = scan.policy ?? {};
215
+ const level = scan.level ?? "info";
216
+
217
+ document.body.className = `level-${level}`;
218
+ elements.targetName.textContent = result.displayTarget ?? scan.target ?? "Scan result";
219
+ elements.sourcePill.textContent = result.source ?? "scan";
220
+ elements.score.textContent = scan.score ?? 0;
221
+ elements.level.textContent = level;
222
+ elements.decision.textContent = displayLabel(policy.decision ?? "allow");
223
+ elements.reason.textContent = policy.reason ?? "";
224
+ elements.critical.textContent = summary.critical ?? 0;
225
+ elements.high.textContent = summary.high ?? 0;
226
+ elements.medium.textContent = summary.medium ?? 0;
227
+ elements.low.textContent = summary.low ?? 0;
228
+ elements.files.textContent = scan.filesScanned ?? 0;
229
+ elements.workspace.textContent = scan.workspace?.skills?.length ?? 0;
230
+ elements.clawhub.textContent = scan.clawhub?.entries?.length ?? 0;
231
+ elements.dependencies.textContent = scan.dependencies?.manifests?.length ?? 0;
232
+ elements.actions.innerHTML = (policy.requiredActions ?? []).map((action) => `<span class="tag">${escapeHtml(displayLabel(action))}</span>`).join("");
233
+ elements.copyJson.textContent = "Copy JSON";
234
+ elements.copyJson.disabled = false;
235
+ elements.downloadHtml.textContent = "Download HTML";
236
+ elements.downloadHtml.disabled = false;
237
+
238
+ renderFindings(scan.findings ?? []);
239
+ }
240
+
241
+ function renderFindings(findings) {
242
+ if (findings.length === 0) {
243
+ elements.findings.className = "findings empty-state";
244
+ elements.findings.textContent = "No risky patterns detected.";
245
+ return;
246
+ }
247
+
248
+ elements.findings.className = "findings";
249
+ elements.findings.innerHTML = findings.map((finding) => `
250
+ <article class="finding-card">
251
+ <div class="finding-top">
252
+ <div>
253
+ <h3>${escapeHtml(finding.title)}</h3>
254
+ <div class="location">${escapeHtml(finding.file)}:${escapeHtml(finding.line)}</div>
255
+ </div>
256
+ <span class="severity ${escapeHtml(finding.severity)}">${escapeHtml(finding.severity)}</span>
257
+ </div>
258
+ <div class="evidence">${escapeHtml(finding.evidence)}</div>
259
+ <p class="recommendation">${escapeHtml(finding.recommendation)}</p>
260
+ </article>
261
+ `).join("");
262
+ }
263
+
264
+ function renderError(error) {
265
+ elements.findings.className = "findings empty-state";
266
+ elements.findings.textContent = error.message;
267
+ }
268
+
269
+ function setBusy(isBusy) {
270
+ elements.scanPaste.disabled = isBusy;
271
+ elements.scanFolder.disabled = isBusy || elements.folderInput.files.length === 0;
272
+ elements.downloadHtml.disabled = isBusy || !state.lastResult;
273
+ for (const button of elements.examples.querySelectorAll("button")) {
274
+ button.disabled = isBusy;
275
+ }
276
+ }
277
+
278
+ async function readSelectedFiles(files) {
279
+ if (files.length > 200) {
280
+ throw new Error("Folder has too many files for the demo scanner.");
281
+ }
282
+
283
+ const output = [];
284
+ let totalBytes = 0;
285
+
286
+ for (const file of files) {
287
+ if (file.size > 512 * 1024) {
288
+ continue;
289
+ }
290
+
291
+ totalBytes += file.size;
292
+ if (totalBytes > 1024 * 1024) {
293
+ throw new Error("Folder content is too large for the demo scanner.");
294
+ }
295
+
296
+ output.push({
297
+ path: file.webkitRelativePath || file.name,
298
+ text: await file.text()
299
+ });
300
+ }
301
+
302
+ return output;
303
+ }
304
+
305
+ function folderLabelFor(files) {
306
+ const firstPath = files[0]?.webkitRelativePath ?? "";
307
+ return firstPath.split("/")[0] || "Uploaded folder";
308
+ }
309
+
310
+ function setCopyButtonText(text) {
311
+ elements.copyJson.textContent = text;
312
+ window.setTimeout(() => {
313
+ elements.copyJson.textContent = "Copy JSON";
314
+ }, 1200);
315
+ }
316
+
317
+ function setDownloadButtonText(text) {
318
+ elements.downloadHtml.textContent = text;
319
+ window.setTimeout(() => {
320
+ elements.downloadHtml.textContent = "Download HTML";
321
+ }, 1200);
322
+ }
323
+
324
+ function downloadText(filename, text, type) {
325
+ const blob = new Blob([text], { type });
326
+ const url = URL.createObjectURL(blob);
327
+ const link = document.createElement("a");
328
+
329
+ link.href = url;
330
+ link.download = filename;
331
+ document.body.append(link);
332
+ link.click();
333
+ link.remove();
334
+ URL.revokeObjectURL(url);
335
+ }
336
+
337
+ function safeFilename(value) {
338
+ return String(value || "scan")
339
+ .replace(/[^A-Za-z0-9_.-]/g, "-")
340
+ .replace(/-+/g, "-")
341
+ .replace(/^-|-$/g, "")
342
+ .slice(0, 80) || "scan";
343
+ }
344
+
345
+ async function fetchJson(url, options = {}) {
346
+ const response = await fetch(url, {
347
+ headers: {
348
+ "content-type": "application/json"
349
+ },
350
+ ...options
351
+ });
352
+ const data = await response.json();
353
+
354
+ if (!response.ok) {
355
+ throw new Error(data.error ?? "Request failed");
356
+ }
357
+
358
+ return data;
359
+ }
360
+
361
+ function escapeHtml(value) {
362
+ return String(value ?? "")
363
+ .replaceAll("&", "&amp;")
364
+ .replaceAll("<", "&lt;")
365
+ .replaceAll(">", "&gt;")
366
+ .replaceAll('"', "&quot;")
367
+ .replaceAll("'", "&#39;");
368
+ }
369
+
370
+ function displayLabel(value) {
371
+ return String(value ?? "")
372
+ .replace(/[_-]+/g, " ")
373
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
374
+ }
package/web/index.html ADDED
@@ -0,0 +1,119 @@
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">
6
+ <title>ClawGuard Web Demo</title>
7
+ <link rel="stylesheet" href="/styles.css">
8
+ </head>
9
+ <body>
10
+ <main class="shell">
11
+ <section class="workbench" aria-label="ClawGuard scanner">
12
+ <aside class="input-pane">
13
+ <div class="brand-row">
14
+ <div class="mark" aria-hidden="true">CG</div>
15
+ <div>
16
+ <h1>ClawGuard</h1>
17
+ <p>OpenClaw skill and tool risk scanner</p>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="control-row">
22
+ <label for="policy">Policy</label>
23
+ <select id="policy">
24
+ <option value="personal">Personal</option>
25
+ <option value="governed">Governed</option>
26
+ <option value="enterprise">Enterprise</option>
27
+ </select>
28
+ </div>
29
+
30
+ <div class="editor-head">
31
+ <label for="skill-input">Paste SKILL.md</label>
32
+ <button id="load-sample" class="ghost" type="button">Load sample</button>
33
+ </div>
34
+ <textarea id="skill-input" spellcheck="false" placeholder="Paste an OpenClaw SKILL.md here"></textarea>
35
+
36
+ <div class="actions">
37
+ <button id="scan-paste" type="button">Scan Paste</button>
38
+ <button id="clear-input" class="ghost" type="button">Clear</button>
39
+ </div>
40
+
41
+ <div class="folder-box">
42
+ <div>
43
+ <label for="folder-input">Skill Folder</label>
44
+ <p id="folder-status">Choose a local folder to scan its files.</p>
45
+ </div>
46
+ <input id="folder-input" type="file" webkitdirectory directory multiple>
47
+ <button id="scan-folder" class="ghost" type="button" disabled>Scan Folder</button>
48
+ </div>
49
+
50
+ <div>
51
+ <h2>Examples</h2>
52
+ <div id="examples" class="examples" aria-live="polite"></div>
53
+ </div>
54
+ </aside>
55
+
56
+ <section class="result-pane" aria-label="Scan result">
57
+ <div class="status-strip">
58
+ <div>
59
+ <p class="eyebrow">Current Target</p>
60
+ <h2 id="target-name">No scan yet</h2>
61
+ </div>
62
+ <span id="source-pill" class="pill">idle</span>
63
+ </div>
64
+
65
+ <section class="score-panel">
66
+ <div class="score-ring" aria-label="Risk score">
67
+ <strong id="score">0</strong>
68
+ <span id="level">info</span>
69
+ </div>
70
+ <div class="decision">
71
+ <p class="eyebrow">Policy Decision</p>
72
+ <h3 id="decision">allow</h3>
73
+ <p id="reason">Paste a skill or choose an example to scan.</p>
74
+ <div id="actions" class="action-tags"></div>
75
+ </div>
76
+ </section>
77
+
78
+ <section class="metrics" aria-label="Finding summary">
79
+ <div><span>Critical</span><strong id="critical-count">0</strong></div>
80
+ <div><span>High</span><strong id="high-count">0</strong></div>
81
+ <div><span>Medium</span><strong id="medium-count">0</strong></div>
82
+ <div><span>Low</span><strong id="low-count">0</strong></div>
83
+ </section>
84
+
85
+ <section class="metadata-grid" aria-label="Metadata summary">
86
+ <div>
87
+ <span>Files</span>
88
+ <strong id="files-count">0</strong>
89
+ </div>
90
+ <div>
91
+ <span>Workspace Skills</span>
92
+ <strong id="workspace-count">0</strong>
93
+ </div>
94
+ <div>
95
+ <span>ClawHub Entries</span>
96
+ <strong id="clawhub-count">0</strong>
97
+ </div>
98
+ <div>
99
+ <span>Dependency Manifests</span>
100
+ <strong id="dependency-count">0</strong>
101
+ </div>
102
+ </section>
103
+
104
+ <section>
105
+ <div class="section-head">
106
+ <h2>Findings</h2>
107
+ <div class="report-actions">
108
+ <button id="download-html" class="ghost" type="button" disabled>Download HTML</button>
109
+ <button id="copy-json" class="ghost" type="button" disabled>Copy JSON</button>
110
+ </div>
111
+ </div>
112
+ <div id="findings" class="findings empty-state">Scan results will appear here.</div>
113
+ </section>
114
+ </section>
115
+ </section>
116
+ </main>
117
+ <script src="/app.js" type="module"></script>
118
+ </body>
119
+ </html>