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 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
- └── app.ts # Main application logic (TypeScript)
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
- .getElementById("show-success-btn")
160
- ?.addEventListener("click", () => showNotification("Success!", "Operation completed successfully", "success"));
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- if (!currentTerminal) {
602
- await showNotification("No Terminal", "Please create a terminal first", "warning");
603
- return;
604
- }
605
- try {
606
- const isWindows = navigator.platform.toLowerCase().includes("win");
607
- const command = isWindows ? "dir" : "ls -la";
608
- const output = document.getElementById("terminal-output");
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
- if (!currentTerminal)
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
- if (!currentTerminal || data.terminalId !== currentTerminal.id)
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
- if (!currentTerminal || data.terminalId !== currentTerminal.id)
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`;