pptb-standard-sample-tool 1.1.2 → 1.1.5
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 +42 -1
- package/dist/app.js +78 -360
- package/dist/features/filesystem.js +291 -0
- package/dist/features/terminal.js +141 -0
- package/dist/icon/icon/sample-icon.png +0 -0
- package/dist/icon/icon/sample-icon.svg +2 -0
- package/dist/index.html +173 -156
- package/dist/security/policy.js +117 -0
- package/dist/security/reporting.js +67 -0
- package/dist/security/suites.js +281 -0
- package/dist/utils/time.js +3 -0
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -56,7 +56,10 @@ This compiles the TypeScript source in `src/` to JavaScript in `dist/`.
|
|
|
56
56
|
```
|
|
57
57
|
html-sample/
|
|
58
58
|
├── src/
|
|
59
|
-
│
|
|
59
|
+
│ ├── app.ts # Main application logic (TypeScript)
|
|
60
|
+
│ ├── features/ # UI feature modules (terminal, filesystem, etc.)
|
|
61
|
+
│ ├── security/ # Security policy + test suites
|
|
62
|
+
│ └── utils/ # Small reusable helpers
|
|
60
63
|
├── index.html # Main HTML file (entry point)
|
|
61
64
|
├── styles.css # Stylesheet
|
|
62
65
|
├── package.json # Package configuration
|
|
@@ -94,6 +97,8 @@ html-sample/
|
|
|
94
97
|
**Terminal:**
|
|
95
98
|
- Create isolated terminal instances
|
|
96
99
|
- Execute shell commands
|
|
100
|
+
- Run a safe terminal security probe (non-destructive)
|
|
101
|
+
- Run API-specific security test suites with pass/fail JSON reports
|
|
97
102
|
- View command output
|
|
98
103
|
- Close terminal when done
|
|
99
104
|
|
|
@@ -150,6 +155,42 @@ const dataverse: typeof window.dataverseAPI = window.dataverseAPI;
|
|
|
150
155
|
3. **Rebuild:** Run `npm run build`
|
|
151
156
|
4. **Reload Tool:** In Power Platform Tool Box, close and reopen the tool
|
|
152
157
|
|
|
158
|
+
## Security Testing Guidance
|
|
159
|
+
|
|
160
|
+
Use the built-in **Run Security Probe** button in the Terminal section to validate terminal exposure with safe commands only.
|
|
161
|
+
|
|
162
|
+
What it checks:
|
|
163
|
+
- Terminal can be created and receives command output
|
|
164
|
+
- Basic command execution works
|
|
165
|
+
- Command chaining is possible (risk signal if unrestricted)
|
|
166
|
+
|
|
167
|
+
What it does not do:
|
|
168
|
+
- No destructive commands
|
|
169
|
+
- No credential, SSH, or private file access attempts
|
|
170
|
+
- No process memory dumping attempts
|
|
171
|
+
|
|
172
|
+
If the probe succeeds, treat that as a signal to enforce stricter host-side controls in Power Platform Tool Box:
|
|
173
|
+
- Command allow-listing
|
|
174
|
+
- Path allow-listing for filesystem APIs
|
|
175
|
+
- Auditing/logging for terminal command execution
|
|
176
|
+
|
|
177
|
+
This sample now includes policy guards in [src/security/policy.ts](src/security/policy.ts):
|
|
178
|
+
- `getBlockedCommandReason(...)` / `getBlockedPathReason(...)`: policy decisions
|
|
179
|
+
- `executeCommandWithPolicyGuard(...)` / `readTextWithPolicyGuard(...)`: central enforcement wrappers
|
|
180
|
+
|
|
181
|
+
Security suites and report formatting live in:
|
|
182
|
+
- [src/security/suites.ts](src/security/suites.ts)
|
|
183
|
+
- [src/security/reporting.ts](src/security/reporting.ts)
|
|
184
|
+
|
|
185
|
+
It also includes **API-specific Security Suite** buttons plus an **All Suites** runner. Each suite emits a JSON report with per-test `severity` and a rolled-up `highestSeverity`:
|
|
186
|
+
- **Terminal suite:** command allow-list enforcement, multiline/control-character blocking, overlong command blocking, local-data-to-network exfil pattern blocking, burst handling
|
|
187
|
+
- **FileSystem suite:** absolute-path enforcement, traversal blocking, sensitive path blocking, guarded read rejection checks, API surface checks
|
|
188
|
+
- **Events suite:** event API presence and malformed payload resilience checks
|
|
189
|
+
- **Settings suite:** API surface checks, set/get roundtrip checks, optional setAll/getAll validation
|
|
190
|
+
- **Dataverse suite:** method surface checks and read-only runtime checks (WhoAmI/query) when connected
|
|
191
|
+
|
|
192
|
+
For production PPTB host hardening, apply equivalent checks in the host process (server-side / main-process boundary), not only in tool UI code.
|
|
193
|
+
|
|
153
194
|
## API Usage Examples
|
|
154
195
|
|
|
155
196
|
### Advanced Utilities
|
package/dist/app.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/// <reference types="@pptb/types" />
|
|
2
|
+
import { createFileSystemFeature } from "./features/filesystem.js";
|
|
3
|
+
import { createTerminalFeature } from "./features/terminal.js";
|
|
4
|
+
import { createSecuritySuites } from "./security/suites.js";
|
|
3
5
|
/**
|
|
4
6
|
* HTML Sample Tool for Power Platform Tool Box
|
|
5
7
|
*
|
|
@@ -18,6 +20,9 @@ let currentConnection = null;
|
|
|
18
20
|
let secondaryConnection = null;
|
|
19
21
|
let currentTerminal = null;
|
|
20
22
|
let createdId = null;
|
|
23
|
+
let securitySuites = null;
|
|
24
|
+
let terminalFeature = null;
|
|
25
|
+
let fileSystemFeature = null;
|
|
21
26
|
/**
|
|
22
27
|
* Initialize the application
|
|
23
28
|
*/
|
|
@@ -28,6 +33,33 @@ async function initialize() {
|
|
|
28
33
|
await refreshConnection();
|
|
29
34
|
// Subscribe to events
|
|
30
35
|
subscribeToEvents();
|
|
36
|
+
terminalFeature = createTerminalFeature({
|
|
37
|
+
toolbox,
|
|
38
|
+
showNotification,
|
|
39
|
+
log,
|
|
40
|
+
getCurrentTerminal: () => currentTerminal,
|
|
41
|
+
setCurrentTerminal: (t) => {
|
|
42
|
+
currentTerminal = t;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
fileSystemFeature = createFileSystemFeature({
|
|
46
|
+
toolbox,
|
|
47
|
+
showNotification,
|
|
48
|
+
log,
|
|
49
|
+
getCurrentConnection: () => currentConnection,
|
|
50
|
+
});
|
|
51
|
+
// Initialize security suites (used by UI buttons)
|
|
52
|
+
securitySuites = createSecuritySuites({
|
|
53
|
+
toolbox,
|
|
54
|
+
dataverse,
|
|
55
|
+
getCurrentConnection: () => currentConnection,
|
|
56
|
+
getCurrentTerminal: () => currentTerminal,
|
|
57
|
+
createTerminal,
|
|
58
|
+
handleTerminalOutput,
|
|
59
|
+
handleCommandCompleted,
|
|
60
|
+
showNotification,
|
|
61
|
+
log,
|
|
62
|
+
});
|
|
31
63
|
// Setup UI event handlers
|
|
32
64
|
setupEventHandlers();
|
|
33
65
|
// Apply theme
|
|
@@ -77,8 +109,7 @@ async function refreshConnection() {
|
|
|
77
109
|
}
|
|
78
110
|
else {
|
|
79
111
|
connectionInfo.className = "info-box warning";
|
|
80
|
-
connectionInfo.innerHTML =
|
|
81
|
-
"<p><strong>⚠️ No active connection</strong><br>Please connect to a Dataverse environment to use this tool.</p>";
|
|
112
|
+
connectionInfo.innerHTML = "<p><strong>⚠️ No active connection</strong><br>Please connect to a Dataverse environment to use this tool.</p>";
|
|
82
113
|
log("No active connection found", "warning");
|
|
83
114
|
}
|
|
84
115
|
if (secondaryConnection) {
|
|
@@ -114,8 +145,7 @@ async function refreshConnection() {
|
|
|
114
145
|
if (!secondaryInfo)
|
|
115
146
|
return;
|
|
116
147
|
secondaryInfo.className = "info-box warning";
|
|
117
|
-
secondaryInfo.innerHTML =
|
|
118
|
-
"<p><strong>⚠️ No secondary connection</strong><br>Please connect to a secondary Dataverse environment to use this tool.</p>";
|
|
148
|
+
secondaryInfo.innerHTML = "<p><strong>⚠️ No secondary connection</strong><br>Please connect to a secondary Dataverse environment to use this tool.</p>";
|
|
119
149
|
log("No secondary connection found", "warning");
|
|
120
150
|
}
|
|
121
151
|
}
|
|
@@ -155,18 +185,10 @@ function subscribeToEvents() {
|
|
|
155
185
|
*/
|
|
156
186
|
function setupEventHandlers() {
|
|
157
187
|
// Notification buttons
|
|
158
|
-
document
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
document
|
|
162
|
-
.getElementById("show-info-btn")
|
|
163
|
-
?.addEventListener("click", () => showNotification("Information", "This is an informational message", "info"));
|
|
164
|
-
document
|
|
165
|
-
.getElementById("show-warning-btn")
|
|
166
|
-
?.addEventListener("click", () => showNotification("Warning", "Please review this warning", "warning"));
|
|
167
|
-
document
|
|
168
|
-
.getElementById("show-error-btn")
|
|
169
|
-
?.addEventListener("click", () => showNotification("Error", "An error has occurred", "error"));
|
|
188
|
+
document.getElementById("show-success-btn")?.addEventListener("click", () => showNotification("Success!", "Operation completed successfully", "success"));
|
|
189
|
+
document.getElementById("show-info-btn")?.addEventListener("click", () => showNotification("Information", "This is an informational message", "info"));
|
|
190
|
+
document.getElementById("show-warning-btn")?.addEventListener("click", () => showNotification("Warning", "Please review this warning", "warning"));
|
|
191
|
+
document.getElementById("show-error-btn")?.addEventListener("click", () => showNotification("Error", "An error has occurred", "error"));
|
|
170
192
|
document.getElementById("show-loading-btn")?.addEventListener("click", showLoading);
|
|
171
193
|
// Utility buttons
|
|
172
194
|
document.getElementById("copy-clipboard-btn")?.addEventListener("click", copyToClipboard);
|
|
@@ -182,6 +204,13 @@ function setupEventHandlers() {
|
|
|
182
204
|
// Terminal buttons
|
|
183
205
|
document.getElementById("create-terminal-btn")?.addEventListener("click", createTerminal);
|
|
184
206
|
document.getElementById("execute-command-btn")?.addEventListener("click", executeTerminalCommand);
|
|
207
|
+
document.getElementById("run-security-probe-btn")?.addEventListener("click", runTerminalSecurityProbe);
|
|
208
|
+
document.getElementById("run-terminal-suite-btn")?.addEventListener("click", () => securitySuites?.runTerminalSuite());
|
|
209
|
+
document.getElementById("run-filesystem-suite-btn")?.addEventListener("click", () => securitySuites?.runFileSystemSuite());
|
|
210
|
+
document.getElementById("run-events-suite-btn")?.addEventListener("click", () => securitySuites?.runEventsSuite());
|
|
211
|
+
document.getElementById("run-settings-suite-btn")?.addEventListener("click", () => securitySuites?.runSettingsSuite());
|
|
212
|
+
document.getElementById("run-dataverse-suite-btn")?.addEventListener("click", () => securitySuites?.runDataverseSuite());
|
|
213
|
+
document.getElementById("run-security-suite-btn")?.addEventListener("click", () => securitySuites?.runAllSuites());
|
|
185
214
|
document.getElementById("close-terminal-btn")?.addEventListener("click", closeTerminal);
|
|
186
215
|
// Dataverse query button
|
|
187
216
|
document.getElementById("query-accounts-btn")?.addEventListener("click", queryAccounts);
|
|
@@ -264,406 +293,99 @@ async function showCurrentTheme() {
|
|
|
264
293
|
log(`Error getting theme: ${error.message}`, "error");
|
|
265
294
|
}
|
|
266
295
|
}
|
|
296
|
+
function requireTerminalFeature() {
|
|
297
|
+
if (!terminalFeature) {
|
|
298
|
+
throw new Error("Terminal feature not initialized");
|
|
299
|
+
}
|
|
300
|
+
return terminalFeature;
|
|
301
|
+
}
|
|
302
|
+
function requireFileSystemFeature() {
|
|
303
|
+
if (!fileSystemFeature) {
|
|
304
|
+
throw new Error("FileSystem feature not initialized");
|
|
305
|
+
}
|
|
306
|
+
return fileSystemFeature;
|
|
307
|
+
}
|
|
267
308
|
/**
|
|
268
309
|
* Save data to file
|
|
269
310
|
*/
|
|
270
311
|
async function saveDataToFile() {
|
|
271
|
-
|
|
272
|
-
const data = {
|
|
273
|
-
timestamp: new Date().toISOString(),
|
|
274
|
-
connection: currentConnection
|
|
275
|
-
? {
|
|
276
|
-
name: currentConnection.name,
|
|
277
|
-
url: currentConnection.url,
|
|
278
|
-
environment: currentConnection.environment,
|
|
279
|
-
}
|
|
280
|
-
: null,
|
|
281
|
-
message: "Export from HTML Sample Tool",
|
|
282
|
-
};
|
|
283
|
-
const filePath = await toolbox.fileSystem.saveFile("sample-export.json", JSON.stringify(data, null, 2));
|
|
284
|
-
if (filePath) {
|
|
285
|
-
await showNotification("File Saved", `File saved to: ${filePath}`, "success");
|
|
286
|
-
log(`File saved to: ${filePath}`, "success");
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
log("File save cancelled", "info");
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
catch (error) {
|
|
293
|
-
log(`Error saving file: ${error.message}`, "error");
|
|
294
|
-
}
|
|
312
|
+
await requireFileSystemFeature().saveDataToFile();
|
|
295
313
|
}
|
|
296
314
|
/**
|
|
297
315
|
* Read text file
|
|
298
316
|
*/
|
|
299
317
|
async function readText() {
|
|
300
|
-
|
|
301
|
-
const output = document.getElementById("filesystem-output");
|
|
302
|
-
if (output)
|
|
303
|
-
output.textContent = "Selecting text file...\n";
|
|
304
|
-
const filePath = await toolbox.fileSystem.selectPath({
|
|
305
|
-
type: "file",
|
|
306
|
-
title: "Select a Text File",
|
|
307
|
-
filters: [
|
|
308
|
-
{ name: "Text Files", extensions: ["txt", "json", "xml", "csv", "md"] },
|
|
309
|
-
{ name: "All Files", extensions: ["*"] },
|
|
310
|
-
],
|
|
311
|
-
});
|
|
312
|
-
if (!filePath) {
|
|
313
|
-
if (output)
|
|
314
|
-
output.textContent = "File selection cancelled.";
|
|
315
|
-
log("File selection cancelled", "info");
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (output)
|
|
319
|
-
output.textContent = `Reading file: ${filePath}\n\n`;
|
|
320
|
-
const content = await toolbox.fileSystem.readText(filePath);
|
|
321
|
-
if (output) {
|
|
322
|
-
output.textContent += `File Size: ${content.length} characters\n\n`;
|
|
323
|
-
output.textContent += "Content:\n";
|
|
324
|
-
output.textContent += "─".repeat(50) + "\n";
|
|
325
|
-
output.textContent += content;
|
|
326
|
-
}
|
|
327
|
-
await showNotification("Success", `File read successfully (${content.length} characters)`, "success");
|
|
328
|
-
log(`Read text file: ${filePath} (${content.length} chars)`, "success");
|
|
329
|
-
}
|
|
330
|
-
catch (error) {
|
|
331
|
-
const output = document.getElementById("filesystem-output");
|
|
332
|
-
if (output)
|
|
333
|
-
output.textContent = `Error: ${error.message}`;
|
|
334
|
-
log(`Error reading text file: ${error.message}`, "error");
|
|
335
|
-
}
|
|
318
|
+
await requireFileSystemFeature().readText();
|
|
336
319
|
}
|
|
337
320
|
/**
|
|
338
321
|
* Read directory contents
|
|
339
322
|
*/
|
|
340
323
|
async function readDirectory() {
|
|
341
|
-
|
|
342
|
-
const output = document.getElementById("filesystem-output");
|
|
343
|
-
if (output)
|
|
344
|
-
output.textContent = "Selecting directory...\n";
|
|
345
|
-
const dirPath = await toolbox.fileSystem.selectPath({
|
|
346
|
-
type: "folder",
|
|
347
|
-
title: "Select a Directory",
|
|
348
|
-
});
|
|
349
|
-
if (!dirPath) {
|
|
350
|
-
if (output)
|
|
351
|
-
output.textContent = "Directory selection cancelled.";
|
|
352
|
-
log("Directory selection cancelled", "info");
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
if (output)
|
|
356
|
-
output.textContent = `Reading directory: ${dirPath}\n\n`;
|
|
357
|
-
const entries = await toolbox.fileSystem.readDirectory(dirPath);
|
|
358
|
-
// Separate files and directories
|
|
359
|
-
const directories = entries.filter((e) => e.type === "directory");
|
|
360
|
-
const files = entries.filter((e) => e.type === "file");
|
|
361
|
-
if (output) {
|
|
362
|
-
output.textContent += `Found ${entries.length} entries:\n`;
|
|
363
|
-
output.textContent += "─".repeat(50) + "\n";
|
|
364
|
-
if (directories.length > 0) {
|
|
365
|
-
output.textContent += `\nDirectories (${directories.length}):\n`;
|
|
366
|
-
directories.forEach((entry) => {
|
|
367
|
-
output.textContent += ` 📁 ${entry.name}\n`;
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
if (files.length > 0) {
|
|
371
|
-
output.textContent += `\nFiles (${files.length}):\n`;
|
|
372
|
-
files.forEach((entry) => {
|
|
373
|
-
output.textContent += ` 📄 ${entry.name}\n`;
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
await showNotification("Success", `Directory read successfully (${entries.length} entries)`, "success");
|
|
378
|
-
log(`Read directory: ${dirPath} (${entries.length} entries: ${directories.length} dirs, ${files.length} files)`, "success");
|
|
379
|
-
}
|
|
380
|
-
catch (error) {
|
|
381
|
-
const output = document.getElementById("filesystem-output");
|
|
382
|
-
if (output)
|
|
383
|
-
output.textContent = `Error: ${error.message}`;
|
|
384
|
-
log(`Error reading directory: ${error.message}`, "error");
|
|
385
|
-
}
|
|
324
|
+
await requireFileSystemFeature().readDirectory();
|
|
386
325
|
}
|
|
387
326
|
/**
|
|
388
327
|
* Create directory
|
|
389
328
|
*/
|
|
390
329
|
async function createDirectory() {
|
|
391
|
-
|
|
392
|
-
const output = document.getElementById("filesystem-output");
|
|
393
|
-
if (output)
|
|
394
|
-
output.textContent = "Selecting parent directory...\n";
|
|
395
|
-
const parentPath = await toolbox.fileSystem.selectPath({
|
|
396
|
-
type: "folder",
|
|
397
|
-
title: "Select Parent Directory",
|
|
398
|
-
});
|
|
399
|
-
if (!parentPath) {
|
|
400
|
-
if (output)
|
|
401
|
-
output.textContent = "Directory selection cancelled.";
|
|
402
|
-
log("Directory selection cancelled", "info");
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
// Use built-in prompt for directory name (workaround since browser prompt may not be available)
|
|
406
|
-
// For better UX, you could add an input field to the UI
|
|
407
|
-
const dirName = prompt("Enter new directory name:", "new-folder");
|
|
408
|
-
if (!dirName || dirName.trim() === "") {
|
|
409
|
-
if (output)
|
|
410
|
-
output.textContent = "Directory creation cancelled.";
|
|
411
|
-
log("Directory creation cancelled", "info");
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
const newDirPath = `${parentPath}/${dirName.trim()}`;
|
|
415
|
-
if (output)
|
|
416
|
-
output.textContent = `Creating directory: ${newDirPath}\n`;
|
|
417
|
-
await toolbox.fileSystem.createDirectory(newDirPath);
|
|
418
|
-
if (output) {
|
|
419
|
-
output.textContent += `✓ Directory created successfully!\n`;
|
|
420
|
-
output.textContent += `Path: ${newDirPath}`;
|
|
421
|
-
}
|
|
422
|
-
await showNotification("Success", `Directory created: ${newDirPath}`, "success");
|
|
423
|
-
log(`Created directory: ${newDirPath}`, "success");
|
|
424
|
-
}
|
|
425
|
-
catch (error) {
|
|
426
|
-
const output = document.getElementById("filesystem-output");
|
|
427
|
-
if (output)
|
|
428
|
-
output.textContent = `Error: ${error.message}`;
|
|
429
|
-
log(`Error creating directory: ${error.message}`, "error");
|
|
430
|
-
}
|
|
330
|
+
await requireFileSystemFeature().createDirectory();
|
|
431
331
|
}
|
|
432
332
|
/**
|
|
433
333
|
* Read system file (hardcoded macOS path for testing)
|
|
434
334
|
* This tests error handling when accessing restricted files
|
|
435
335
|
*/
|
|
436
336
|
async function readSystemFile() {
|
|
437
|
-
|
|
438
|
-
const output = document.getElementById("filesystem-output");
|
|
439
|
-
const systemFilePath = "/var/log/system.log";
|
|
440
|
-
if (output)
|
|
441
|
-
output.textContent = `Attempting to read system file: ${systemFilePath}\n\n`;
|
|
442
|
-
log(`Attempting to read system file: ${systemFilePath}`, "info");
|
|
443
|
-
const content = await toolbox.fileSystem.readText(systemFilePath);
|
|
444
|
-
if (output) {
|
|
445
|
-
output.textContent += `File Size: ${content.length} characters\n\n`;
|
|
446
|
-
output.textContent += "Content (first 1000 chars):\n";
|
|
447
|
-
output.textContent += "─".repeat(50) + "\n";
|
|
448
|
-
output.textContent += content.substring(0, 1000);
|
|
449
|
-
if (content.length > 1000) {
|
|
450
|
-
output.textContent += "\n\n... (truncated)";
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
await showNotification("Success", `System file read (${content.length} characters)`, "success");
|
|
454
|
-
log(`Successfully read system file: ${systemFilePath}`, "success");
|
|
455
|
-
}
|
|
456
|
-
catch (error) {
|
|
457
|
-
const output = document.getElementById("filesystem-output");
|
|
458
|
-
const errorMsg = error.message;
|
|
459
|
-
if (output) {
|
|
460
|
-
output.textContent = `❌ Error Reading System File\n`;
|
|
461
|
-
output.textContent += `Path: /var/log/system.log\n\n`;
|
|
462
|
-
output.textContent += `Error: ${errorMsg}\n\n`;
|
|
463
|
-
output.textContent += "This is expected - system files are typically restricted due to:\n";
|
|
464
|
-
output.textContent += "- File permissions (insufficient access)\n";
|
|
465
|
-
output.textContent += "- Security restrictions\n";
|
|
466
|
-
output.textContent += "- Sandbox limitations";
|
|
467
|
-
}
|
|
468
|
-
log(`Error reading system file: ${errorMsg}`, "error");
|
|
469
|
-
await showNotification("Error Reading System File", errorMsg, "error");
|
|
470
|
-
}
|
|
337
|
+
await requireFileSystemFeature().readSystemFile();
|
|
471
338
|
}
|
|
472
339
|
/**
|
|
473
340
|
* Read file with selection dialog, but then hardcode a macOS path instead
|
|
474
341
|
* This tests ignoring user selection and attempting to read a restricted path
|
|
475
342
|
*/
|
|
476
343
|
async function readHardcodedFile() {
|
|
477
|
-
|
|
478
|
-
const output = document.getElementById("filesystem-output");
|
|
479
|
-
if (output)
|
|
480
|
-
output.textContent = "Opening file selection dialog...\n";
|
|
481
|
-
log("Opening file selection dialog", "info");
|
|
482
|
-
// Open file picker dialog
|
|
483
|
-
const selectedPath = await toolbox.fileSystem.selectPath({
|
|
484
|
-
type: "file",
|
|
485
|
-
title: "Select a File (will be ignored)",
|
|
486
|
-
filters: [{ name: "All Files", extensions: ["*"] }],
|
|
487
|
-
});
|
|
488
|
-
if (!selectedPath) {
|
|
489
|
-
if (output)
|
|
490
|
-
output.textContent = "File selection cancelled.";
|
|
491
|
-
log("File selection cancelled", "info");
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
// Show what was selected
|
|
495
|
-
if (output) {
|
|
496
|
-
output.textContent = `User selected: ${selectedPath}\n\n`;
|
|
497
|
-
output.textContent += "But we're ignoring that and attempting to read a hardcoded system path instead...\n\n";
|
|
498
|
-
output.textContent += "─".repeat(50) + "\n\n";
|
|
499
|
-
}
|
|
500
|
-
log(`User selected: ${selectedPath} (will be ignored)`, "info");
|
|
501
|
-
// Hardcode a macOS system path and ignore the selection
|
|
502
|
-
const hardcodedPath = "/etc/passwd";
|
|
503
|
-
if (output)
|
|
504
|
-
output.textContent += `Attempting to read hardcoded path: ${hardcodedPath}\n\n`;
|
|
505
|
-
log(`Attempting to read hardcoded path: ${hardcodedPath}`, "info");
|
|
506
|
-
const content = await toolbox.fileSystem.readText(hardcodedPath);
|
|
507
|
-
if (output) {
|
|
508
|
-
output.textContent += `✓ Success! File Size: ${content.length} characters\n\n`;
|
|
509
|
-
output.textContent += "Content:\n";
|
|
510
|
-
output.textContent += "─".repeat(50) + "\n";
|
|
511
|
-
output.textContent += content.substring(0, 1500);
|
|
512
|
-
if (content.length > 1500) {
|
|
513
|
-
output.textContent += "\n\n... (truncated)";
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
await showNotification("File Read Successfully", `Hardcoded file read: ${hardcodedPath} (${content.length} characters)`, "success");
|
|
517
|
-
log(`Successfully read hardcoded file: ${hardcodedPath}`, "success");
|
|
518
|
-
}
|
|
519
|
-
catch (error) {
|
|
520
|
-
const output = document.getElementById("filesystem-output");
|
|
521
|
-
const errorMsg = error.message;
|
|
522
|
-
if (output) {
|
|
523
|
-
output.textContent += `❌ Error Reading Hardcoded File\n`;
|
|
524
|
-
output.textContent += `Path: /etc/passwd\n\n`;
|
|
525
|
-
output.textContent += `Error: ${errorMsg}\n\n`;
|
|
526
|
-
output.textContent += "This demonstrates that hardcoded system paths are typically restricted:\n";
|
|
527
|
-
output.textContent += "- Permission denied (macOS sandbox restrictions)\n";
|
|
528
|
-
output.textContent += "- The file picker selected a different path, but we tried to read this one instead\n";
|
|
529
|
-
output.textContent += "- Real-world use case: don't hardcode paths, always use user selection";
|
|
530
|
-
}
|
|
531
|
-
log(`Error reading hardcoded file: ${errorMsg}`, "error");
|
|
532
|
-
await showNotification("Error Reading Hardcoded File", errorMsg, "error");
|
|
533
|
-
}
|
|
344
|
+
await requireFileSystemFeature().readHardcodedFile();
|
|
534
345
|
}
|
|
535
346
|
/**
|
|
536
347
|
* Read a hardcoded macOS file path directly without using selectPath dialog
|
|
537
348
|
* This tests reading a restricted file with no user interaction
|
|
538
349
|
*/
|
|
539
350
|
async function readDirectFile() {
|
|
540
|
-
|
|
541
|
-
const output = document.getElementById("filesystem-output");
|
|
542
|
-
const hardcodedPath = "/etc/hosts";
|
|
543
|
-
if (output)
|
|
544
|
-
output.textContent = `Reading hardcoded path directly (no dialog)...\n`;
|
|
545
|
-
if (output)
|
|
546
|
-
output.textContent += `Path: ${hardcodedPath}\n\n`;
|
|
547
|
-
log(`Attempting direct read of hardcoded path: ${hardcodedPath}`, "info");
|
|
548
|
-
const content = await toolbox.fileSystem.readText(hardcodedPath);
|
|
549
|
-
if (output) {
|
|
550
|
-
output.textContent += `✓ Success! File Size: ${content.length} characters\n\n`;
|
|
551
|
-
output.textContent += "Content:\n";
|
|
552
|
-
output.textContent += "─".repeat(50) + "\n";
|
|
553
|
-
output.textContent += content;
|
|
554
|
-
}
|
|
555
|
-
await showNotification("File Read Successfully", `Direct file read: ${hardcodedPath} (${content.length} characters)`, "success");
|
|
556
|
-
log(`Successfully read direct file: ${hardcodedPath}`, "success");
|
|
557
|
-
}
|
|
558
|
-
catch (error) {
|
|
559
|
-
const output = document.getElementById("filesystem-output");
|
|
560
|
-
const errorMsg = error.message;
|
|
561
|
-
if (output) {
|
|
562
|
-
output.textContent = `❌ Error Reading Hardcoded Path\n`;
|
|
563
|
-
output.textContent += `Path: /etc/hosts\n`;
|
|
564
|
-
output.textContent += `Method: Direct read (no selectPath dialog)\n\n`;
|
|
565
|
-
output.textContent += `Error: ${errorMsg}\n\n`;
|
|
566
|
-
output.textContent += "This demonstrates:\n";
|
|
567
|
-
output.textContent += "- System files are protected even with hardcoded paths\n";
|
|
568
|
-
output.textContent += "- No file picker dialog was used\n";
|
|
569
|
-
output.textContent += "- Security restrictions apply to all file access attempts";
|
|
570
|
-
}
|
|
571
|
-
log(`Error reading direct file: ${errorMsg}`, "error");
|
|
572
|
-
await showNotification("Error Reading Direct File", errorMsg, "error");
|
|
573
|
-
}
|
|
351
|
+
await requireFileSystemFeature().readDirectFile();
|
|
574
352
|
}
|
|
575
353
|
/**
|
|
576
354
|
* Create a terminal
|
|
577
355
|
*/
|
|
578
356
|
async function createTerminal() {
|
|
579
|
-
|
|
580
|
-
currentTerminal = await toolbox.terminal.create({
|
|
581
|
-
name: "HTML Sample Terminal",
|
|
582
|
-
});
|
|
583
|
-
log(`Terminal created: ${currentTerminal.name} (${currentTerminal.id})`, "success");
|
|
584
|
-
// Enable command buttons
|
|
585
|
-
const executeBtn = document.getElementById("execute-command-btn");
|
|
586
|
-
const closeBtn = document.getElementById("close-terminal-btn");
|
|
587
|
-
if (executeBtn)
|
|
588
|
-
executeBtn.disabled = false;
|
|
589
|
-
if (closeBtn)
|
|
590
|
-
closeBtn.disabled = false;
|
|
591
|
-
await showNotification("Terminal Created", `Terminal ${currentTerminal.name} is ready`, "success");
|
|
592
|
-
}
|
|
593
|
-
catch (error) {
|
|
594
|
-
log(`Error creating terminal: ${error.message}`, "error");
|
|
595
|
-
}
|
|
357
|
+
await requireTerminalFeature().createTerminal();
|
|
596
358
|
}
|
|
597
359
|
/**
|
|
598
360
|
* Execute terminal command
|
|
599
361
|
*/
|
|
600
362
|
async function executeTerminalCommand() {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (output) {
|
|
610
|
-
output.textContent = `> ${command}\n`;
|
|
611
|
-
}
|
|
612
|
-
log(`Executing command: ${command}`, "info");
|
|
613
|
-
await toolbox.terminal.execute(currentTerminal.id, command);
|
|
614
|
-
}
|
|
615
|
-
catch (error) {
|
|
616
|
-
log(`Error executing command: ${error.message}`, "error");
|
|
617
|
-
}
|
|
363
|
+
await requireTerminalFeature().executeTerminalCommand();
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Run a safe terminal security probe.
|
|
367
|
+
* This validates terminal exposure and command chaining with non-malicious commands only.
|
|
368
|
+
*/
|
|
369
|
+
async function runTerminalSecurityProbe() {
|
|
370
|
+
await requireTerminalFeature().runTerminalSecurityProbe();
|
|
618
371
|
}
|
|
619
372
|
/**
|
|
620
373
|
* Close terminal
|
|
621
374
|
*/
|
|
622
375
|
async function closeTerminal() {
|
|
623
|
-
|
|
624
|
-
return;
|
|
625
|
-
try {
|
|
626
|
-
await toolbox.terminal.close(currentTerminal.id);
|
|
627
|
-
log("Terminal closed", "info");
|
|
628
|
-
currentTerminal = null;
|
|
629
|
-
// Disable command buttons
|
|
630
|
-
const executeBtn = document.getElementById("execute-command-btn");
|
|
631
|
-
const closeBtn = document.getElementById("close-terminal-btn");
|
|
632
|
-
if (executeBtn)
|
|
633
|
-
executeBtn.disabled = true;
|
|
634
|
-
if (closeBtn)
|
|
635
|
-
closeBtn.disabled = true;
|
|
636
|
-
const output = document.getElementById("terminal-output");
|
|
637
|
-
if (output)
|
|
638
|
-
output.textContent = "";
|
|
639
|
-
}
|
|
640
|
-
catch (error) {
|
|
641
|
-
log(`Error closing terminal: ${error.message}`, "error");
|
|
642
|
-
}
|
|
376
|
+
await requireTerminalFeature().closeTerminal();
|
|
643
377
|
}
|
|
644
378
|
/**
|
|
645
379
|
* Handle terminal output events
|
|
646
380
|
*/
|
|
647
381
|
function handleTerminalOutput(data) {
|
|
648
|
-
|
|
649
|
-
return;
|
|
650
|
-
const output = document.getElementById("terminal-output");
|
|
651
|
-
if (output) {
|
|
652
|
-
output.textContent += data.data;
|
|
653
|
-
output.scrollTop = output.scrollHeight;
|
|
654
|
-
}
|
|
382
|
+
requireTerminalFeature().handleTerminalOutput(data);
|
|
655
383
|
}
|
|
656
384
|
/**
|
|
657
385
|
* Handle command completed events
|
|
658
386
|
*/
|
|
659
387
|
function handleCommandCompleted(data) {
|
|
660
|
-
|
|
661
|
-
return;
|
|
662
|
-
const output = document.getElementById("terminal-output");
|
|
663
|
-
if (output) {
|
|
664
|
-
output.textContent += `\n[Command completed with exit code: ${data.exitCode}]\n`;
|
|
665
|
-
output.scrollTop = output.scrollHeight;
|
|
666
|
-
}
|
|
388
|
+
requireTerminalFeature().handleCommandCompleted(data);
|
|
667
389
|
}
|
|
668
390
|
/**
|
|
669
391
|
* Query accounts from Dataverse
|
|
@@ -946,11 +668,7 @@ async function getAccountAttributesMetadata() {
|
|
|
946
668
|
try {
|
|
947
669
|
if (output) {
|
|
948
670
|
output.textContent = "Retrieving account attributes metadata...\n";
|
|
949
|
-
const metadata = await dataverse.getEntityRelatedMetadata("account", "Attributes", [
|
|
950
|
-
"LogicalName",
|
|
951
|
-
"DisplayName",
|
|
952
|
-
"AttributeType",
|
|
953
|
-
]);
|
|
671
|
+
const metadata = await dataverse.getEntityRelatedMetadata("account", "Attributes", ["LogicalName", "DisplayName", "AttributeType"]);
|
|
954
672
|
if (metadata) {
|
|
955
673
|
const metadataArray = metadata.value;
|
|
956
674
|
output.textContent += `Found ${metadataArray.length} attributes:\n\n`;
|