pptb-standard-sample-tool 1.1.3 → 1.1.6

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/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,190 +20,9 @@ let currentConnection = null;
18
20
  let secondaryConnection = null;
19
21
  let currentTerminal = null;
20
22
  let createdId = null;
21
- const TERMINAL_ALLOWED_COMMANDS = new Set([
22
- "dir",
23
- "ls -la",
24
- "echo PPTB_SECURITY_PROBE",
25
- "pwd",
26
- "uname -a",
27
- "echo CHAIN_TEST && echo SECOND_COMMAND",
28
- "Get-Location",
29
- "$PSVersionTable.PSVersion.ToString()",
30
- 'Write-Output "CHAIN_TEST"; Write-Output "SECOND_COMMAND"',
31
- ]);
32
- const TERMINAL_BLOCKED_TOKENS = [
33
- "rm ",
34
- "del ",
35
- "format",
36
- "sudo",
37
- "chmod",
38
- "chown",
39
- "powershell -enc",
40
- "invoke-expression",
41
- "downloadstring",
42
- "curl ",
43
- "wget ",
44
- "scp ",
45
- "ssh ",
46
- "id_rsa",
47
- ".ssh",
48
- "procdump",
49
- "memory",
50
- "dump",
51
- ];
52
- const FILE_PATH_BLOCK_LIST = [
53
- "/etc/passwd",
54
- "/etc/shadow",
55
- "/etc/hosts",
56
- "/var/log/system.log",
57
- "/.ssh/",
58
- "\\.ssh\\",
59
- "id_rsa",
60
- "known_hosts",
61
- "c:\\windows\\system32",
62
- ];
63
- function delay(ms) {
64
- return new Promise((resolve) => setTimeout(resolve, ms));
65
- }
66
- function normalizeForPolicy(value) {
67
- return value.trim().replace(/\\/g, "/").toLowerCase();
68
- }
69
- function getBlockedCommandReason(command) {
70
- const normalized = command.trim();
71
- if (TERMINAL_ALLOWED_COMMANDS.has(normalized)) {
72
- return null;
73
- }
74
- const low = normalizeForPolicy(normalized);
75
- const blockedToken = TERMINAL_BLOCKED_TOKENS.find((token) => low.includes(token));
76
- if (blockedToken) {
77
- return `Blocked token detected: ${blockedToken}`;
78
- }
79
- return "Command is not on allow-list";
80
- }
81
- function getBlockedPathReason(filePath) {
82
- const normalized = normalizeForPolicy(filePath);
83
- const blockedMatch = FILE_PATH_BLOCK_LIST.find((token) => normalized.includes(token));
84
- if (blockedMatch) {
85
- return `Sensitive path pattern detected: ${blockedMatch}`;
86
- }
87
- return null;
88
- }
89
- async function executeCommandWithPolicyGuard(terminalId, command) {
90
- const blockedReason = getBlockedCommandReason(command);
91
- if (blockedReason) {
92
- throw new Error(`Terminal command blocked by policy. ${blockedReason}`);
93
- }
94
- await toolbox.terminal.execute(terminalId, command);
95
- }
96
- async function readTextWithPolicyGuard(filePath) {
97
- const blockedReason = getBlockedPathReason(filePath);
98
- if (blockedReason) {
99
- throw new Error(`File path blocked by policy. ${blockedReason}`);
100
- }
101
- return toolbox.fileSystem.readText(filePath);
102
- }
103
- function addSecurityResult(results, name, status, details) {
104
- results.push({ name, status, details });
105
- }
106
- async function runSecurityTestSuite() {
107
- const output = document.getElementById("security-suite-output");
108
- if (output) {
109
- output.textContent = "Running security test suite...\n";
110
- }
111
- const results = [];
112
- try {
113
- // 1) Command allow-list exactness
114
- const blockedByAllowList = getBlockedCommandReason("ls") !== null;
115
- addSecurityResult(results, "command_allowlist_exact_match", blockedByAllowList ? "pass" : "fail", blockedByAllowList
116
- ? "Non-allow-listed command was blocked"
117
- : "Command not in allow-list was unexpectedly allowed");
118
- // 2) Risky token detection
119
- const blockedDangerousToken = getBlockedCommandReason("echo ok && curl http://example.com") !== null;
120
- addSecurityResult(results, "command_blocked_token_detection", blockedDangerousToken ? "pass" : "fail", blockedDangerousToken
121
- ? "Risky token pattern was blocked"
122
- : "Risky token pattern bypassed command policy");
123
- // 3) Path normalization / traversal style checks
124
- const traversalPath = "../../Users/test/.ssh/id_rsa";
125
- const traversalBlocked = getBlockedPathReason(traversalPath) !== null;
126
- addSecurityResult(results, "path_normalization_sensitive_match", traversalBlocked ? "pass" : "fail", traversalBlocked
127
- ? "Sensitive path pattern detected after normalization"
128
- : "Sensitive path pattern was not detected");
129
- // 4) Windows-style sensitive path checks
130
- const windowsSensitivePath = "C:\\Users\\demo\\.ssh\\known_hosts";
131
- const windowsPathBlocked = getBlockedPathReason(windowsSensitivePath) !== null;
132
- addSecurityResult(results, "windows_path_sensitive_match", windowsPathBlocked ? "pass" : "fail", windowsPathBlocked
133
- ? "Windows-style sensitive path pattern detected"
134
- : "Windows-style sensitive path pattern was not detected");
135
- // 5) Malformed terminal output payload handling
136
- try {
137
- handleTerminalOutput(undefined);
138
- addSecurityResult(results, "terminal_output_malformed_payload", "pass", "Malformed terminal output payload did not crash handler");
139
- }
140
- catch (error) {
141
- addSecurityResult(results, "terminal_output_malformed_payload", "fail", `Handler crashed: ${error.message}`);
142
- }
143
- // 6) Malformed command-completed payload handling
144
- try {
145
- handleCommandCompleted(undefined);
146
- addSecurityResult(results, "terminal_completed_malformed_payload", "pass", "Malformed command-completed payload did not crash handler");
147
- }
148
- catch (error) {
149
- addSecurityResult(results, "terminal_completed_malformed_payload", "fail", `Handler crashed: ${error.message}`);
150
- }
151
- // 7) Lightweight terminal burst/rate handling
152
- try {
153
- if (!currentTerminal) {
154
- await createTerminal();
155
- }
156
- if (!currentTerminal) {
157
- addSecurityResult(results, "terminal_burst_probe", "fail", "Could not create terminal for burst probe");
158
- }
159
- else {
160
- const burst = Array.from({ length: 5 }).map((_, i) => `echo PPTB_SECURITY_PROBE_BURST_${i + 1}`);
161
- for (const command of burst) {
162
- try {
163
- await executeCommandWithPolicyGuard(currentTerminal.id, command);
164
- }
165
- catch {
166
- // intentional: this checks if policy blocks unknown commands quickly and safely
167
- }
168
- }
169
- addSecurityResult(results, "terminal_burst_probe", "pass", "Burst of rapid commands processed without app crash");
170
- }
171
- }
172
- catch (error) {
173
- addSecurityResult(results, "terminal_burst_probe", "fail", `Burst probe error: ${error.message}`);
174
- }
175
- const failed = results.filter((r) => r.status === "fail").length;
176
- const passed = results.length - failed;
177
- const report = {
178
- generatedAt: new Date().toISOString(),
179
- summary: {
180
- total: results.length,
181
- passed,
182
- failed,
183
- },
184
- results,
185
- };
186
- if (output) {
187
- output.textContent = JSON.stringify(report, null, 2);
188
- }
189
- if (failed > 0) {
190
- await showNotification("Security Suite Completed", `${failed} test(s) failed. Review JSON report.`, "warning");
191
- log(`Security suite completed with ${failed} failure(s)`, "warning");
192
- }
193
- else {
194
- await showNotification("Security Suite Completed", "All security tests passed", "success");
195
- log("Security suite completed with all tests passing", "success");
196
- }
197
- }
198
- catch (error) {
199
- if (output) {
200
- output.textContent = `Security suite error: ${error.message}`;
201
- }
202
- log(`Security suite execution error: ${error.message}`, "error");
203
- }
204
- }
23
+ let securitySuites = null;
24
+ let terminalFeature = null;
25
+ let fileSystemFeature = null;
205
26
  /**
206
27
  * Initialize the application
207
28
  */
@@ -212,6 +33,33 @@ async function initialize() {
212
33
  await refreshConnection();
213
34
  // Subscribe to events
214
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
+ });
215
63
  // Setup UI event handlers
216
64
  setupEventHandlers();
217
65
  // Apply theme
@@ -261,8 +109,7 @@ async function refreshConnection() {
261
109
  }
262
110
  else {
263
111
  connectionInfo.className = "info-box warning";
264
- connectionInfo.innerHTML =
265
- "<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>";
266
113
  log("No active connection found", "warning");
267
114
  }
268
115
  if (secondaryConnection) {
@@ -298,8 +145,7 @@ async function refreshConnection() {
298
145
  if (!secondaryInfo)
299
146
  return;
300
147
  secondaryInfo.className = "info-box warning";
301
- secondaryInfo.innerHTML =
302
- "<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>";
303
149
  log("No secondary connection found", "warning");
304
150
  }
305
151
  }
@@ -339,18 +185,10 @@ function subscribeToEvents() {
339
185
  */
340
186
  function setupEventHandlers() {
341
187
  // Notification buttons
342
- document
343
- .getElementById("show-success-btn")
344
- ?.addEventListener("click", () => showNotification("Success!", "Operation completed successfully", "success"));
345
- document
346
- .getElementById("show-info-btn")
347
- ?.addEventListener("click", () => showNotification("Information", "This is an informational message", "info"));
348
- document
349
- .getElementById("show-warning-btn")
350
- ?.addEventListener("click", () => showNotification("Warning", "Please review this warning", "warning"));
351
- document
352
- .getElementById("show-error-btn")
353
- ?.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"));
354
192
  document.getElementById("show-loading-btn")?.addEventListener("click", showLoading);
355
193
  // Utility buttons
356
194
  document.getElementById("copy-clipboard-btn")?.addEventListener("click", copyToClipboard);
@@ -367,7 +205,12 @@ function setupEventHandlers() {
367
205
  document.getElementById("create-terminal-btn")?.addEventListener("click", createTerminal);
368
206
  document.getElementById("execute-command-btn")?.addEventListener("click", executeTerminalCommand);
369
207
  document.getElementById("run-security-probe-btn")?.addEventListener("click", runTerminalSecurityProbe);
370
- document.getElementById("run-security-suite-btn")?.addEventListener("click", runSecurityTestSuite);
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());
371
214
  document.getElementById("close-terminal-btn")?.addEventListener("click", closeTerminal);
372
215
  // Dataverse query button
373
216
  document.getElementById("query-accounts-btn")?.addEventListener("click", queryAccounts);
@@ -450,464 +293,99 @@ async function showCurrentTheme() {
450
293
  log(`Error getting theme: ${error.message}`, "error");
451
294
  }
452
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
+ }
453
308
  /**
454
309
  * Save data to file
455
310
  */
456
311
  async function saveDataToFile() {
457
- try {
458
- const data = {
459
- timestamp: new Date().toISOString(),
460
- connection: currentConnection
461
- ? {
462
- name: currentConnection.name,
463
- url: currentConnection.url,
464
- environment: currentConnection.environment,
465
- }
466
- : null,
467
- message: "Export from HTML Sample Tool",
468
- };
469
- const filePath = await toolbox.fileSystem.saveFile("sample-export.json", JSON.stringify(data, null, 2));
470
- if (filePath) {
471
- await showNotification("File Saved", `File saved to: ${filePath}`, "success");
472
- log(`File saved to: ${filePath}`, "success");
473
- }
474
- else {
475
- log("File save cancelled", "info");
476
- }
477
- }
478
- catch (error) {
479
- log(`Error saving file: ${error.message}`, "error");
480
- }
312
+ await requireFileSystemFeature().saveDataToFile();
481
313
  }
482
314
  /**
483
315
  * Read text file
484
316
  */
485
317
  async function readText() {
486
- try {
487
- const output = document.getElementById("filesystem-output");
488
- if (output)
489
- output.textContent = "Selecting text file...\n";
490
- const filePath = await toolbox.fileSystem.selectPath({
491
- type: "file",
492
- title: "Select a Text File",
493
- filters: [
494
- { name: "Text Files", extensions: ["txt", "json", "xml", "csv", "md"] },
495
- { name: "All Files", extensions: ["*"] },
496
- ],
497
- });
498
- if (!filePath) {
499
- if (output)
500
- output.textContent = "File selection cancelled.";
501
- log("File selection cancelled", "info");
502
- return;
503
- }
504
- if (output)
505
- output.textContent = `Reading file: ${filePath}\n\n`;
506
- const content = await readTextWithPolicyGuard(filePath);
507
- if (output) {
508
- output.textContent += `File Size: ${content.length} characters\n\n`;
509
- output.textContent += "Content:\n";
510
- output.textContent += "─".repeat(50) + "\n";
511
- output.textContent += content;
512
- }
513
- await showNotification("Success", `File read successfully (${content.length} characters)`, "success");
514
- log(`Read text file: ${filePath} (${content.length} chars)`, "success");
515
- }
516
- catch (error) {
517
- const output = document.getElementById("filesystem-output");
518
- if (output)
519
- output.textContent = `Error: ${error.message}`;
520
- log(`Error reading text file: ${error.message}`, "error");
521
- }
318
+ await requireFileSystemFeature().readText();
522
319
  }
523
320
  /**
524
321
  * Read directory contents
525
322
  */
526
323
  async function readDirectory() {
527
- try {
528
- const output = document.getElementById("filesystem-output");
529
- if (output)
530
- output.textContent = "Selecting directory...\n";
531
- const dirPath = await toolbox.fileSystem.selectPath({
532
- type: "folder",
533
- title: "Select a Directory",
534
- });
535
- if (!dirPath) {
536
- if (output)
537
- output.textContent = "Directory selection cancelled.";
538
- log("Directory selection cancelled", "info");
539
- return;
540
- }
541
- if (output)
542
- output.textContent = `Reading directory: ${dirPath}\n\n`;
543
- const entries = await toolbox.fileSystem.readDirectory(dirPath);
544
- // Separate files and directories
545
- const directories = entries.filter((e) => e.type === "directory");
546
- const files = entries.filter((e) => e.type === "file");
547
- if (output) {
548
- output.textContent += `Found ${entries.length} entries:\n`;
549
- output.textContent += "─".repeat(50) + "\n";
550
- if (directories.length > 0) {
551
- output.textContent += `\nDirectories (${directories.length}):\n`;
552
- directories.forEach((entry) => {
553
- output.textContent += ` 📁 ${entry.name}\n`;
554
- });
555
- }
556
- if (files.length > 0) {
557
- output.textContent += `\nFiles (${files.length}):\n`;
558
- files.forEach((entry) => {
559
- output.textContent += ` 📄 ${entry.name}\n`;
560
- });
561
- }
562
- }
563
- await showNotification("Success", `Directory read successfully (${entries.length} entries)`, "success");
564
- log(`Read directory: ${dirPath} (${entries.length} entries: ${directories.length} dirs, ${files.length} files)`, "success");
565
- }
566
- catch (error) {
567
- const output = document.getElementById("filesystem-output");
568
- if (output)
569
- output.textContent = `Error: ${error.message}`;
570
- log(`Error reading directory: ${error.message}`, "error");
571
- }
324
+ await requireFileSystemFeature().readDirectory();
572
325
  }
573
326
  /**
574
327
  * Create directory
575
328
  */
576
329
  async function createDirectory() {
577
- try {
578
- const output = document.getElementById("filesystem-output");
579
- if (output)
580
- output.textContent = "Selecting parent directory...\n";
581
- const parentPath = await toolbox.fileSystem.selectPath({
582
- type: "folder",
583
- title: "Select Parent Directory",
584
- });
585
- if (!parentPath) {
586
- if (output)
587
- output.textContent = "Directory selection cancelled.";
588
- log("Directory selection cancelled", "info");
589
- return;
590
- }
591
- // Use built-in prompt for directory name (workaround since browser prompt may not be available)
592
- // For better UX, you could add an input field to the UI
593
- const dirName = prompt("Enter new directory name:", "new-folder");
594
- if (!dirName || dirName.trim() === "") {
595
- if (output)
596
- output.textContent = "Directory creation cancelled.";
597
- log("Directory creation cancelled", "info");
598
- return;
599
- }
600
- const newDirPath = `${parentPath}/${dirName.trim()}`;
601
- if (output)
602
- output.textContent = `Creating directory: ${newDirPath}\n`;
603
- await toolbox.fileSystem.createDirectory(newDirPath);
604
- if (output) {
605
- output.textContent += `✓ Directory created successfully!\n`;
606
- output.textContent += `Path: ${newDirPath}`;
607
- }
608
- await showNotification("Success", `Directory created: ${newDirPath}`, "success");
609
- log(`Created directory: ${newDirPath}`, "success");
610
- }
611
- catch (error) {
612
- const output = document.getElementById("filesystem-output");
613
- if (output)
614
- output.textContent = `Error: ${error.message}`;
615
- log(`Error creating directory: ${error.message}`, "error");
616
- }
330
+ await requireFileSystemFeature().createDirectory();
617
331
  }
618
332
  /**
619
333
  * Read system file (hardcoded macOS path for testing)
620
334
  * This tests error handling when accessing restricted files
621
335
  */
622
336
  async function readSystemFile() {
623
- try {
624
- const output = document.getElementById("filesystem-output");
625
- const systemFilePath = "/var/log/system.log";
626
- if (output)
627
- output.textContent = `Attempting to read system file: ${systemFilePath}\n\n`;
628
- log(`Attempting to read system file: ${systemFilePath}`, "info");
629
- const content = await readTextWithPolicyGuard(systemFilePath);
630
- if (output) {
631
- output.textContent += `File Size: ${content.length} characters\n\n`;
632
- output.textContent += "Content (first 1000 chars):\n";
633
- output.textContent += "─".repeat(50) + "\n";
634
- output.textContent += content.substring(0, 1000);
635
- if (content.length > 1000) {
636
- output.textContent += "\n\n... (truncated)";
637
- }
638
- }
639
- await showNotification("Success", `System file read (${content.length} characters)`, "success");
640
- log(`Successfully read system file: ${systemFilePath}`, "success");
641
- }
642
- catch (error) {
643
- const output = document.getElementById("filesystem-output");
644
- const errorMsg = error.message;
645
- if (output) {
646
- output.textContent = `❌ Error Reading System File\n`;
647
- output.textContent += `Path: /var/log/system.log\n\n`;
648
- output.textContent += `Error: ${errorMsg}\n\n`;
649
- output.textContent += "This is expected - system files are typically restricted due to:\n";
650
- output.textContent += "- File permissions (insufficient access)\n";
651
- output.textContent += "- Security restrictions\n";
652
- output.textContent += "- Sandbox limitations";
653
- }
654
- log(`Error reading system file: ${errorMsg}`, "error");
655
- await showNotification("Error Reading System File", errorMsg, "error");
656
- }
337
+ await requireFileSystemFeature().readSystemFile();
657
338
  }
658
339
  /**
659
340
  * Read file with selection dialog, but then hardcode a macOS path instead
660
341
  * This tests ignoring user selection and attempting to read a restricted path
661
342
  */
662
343
  async function readHardcodedFile() {
663
- try {
664
- const output = document.getElementById("filesystem-output");
665
- if (output)
666
- output.textContent = "Opening file selection dialog...\n";
667
- log("Opening file selection dialog", "info");
668
- // Open file picker dialog
669
- const selectedPath = await toolbox.fileSystem.selectPath({
670
- type: "file",
671
- title: "Select a File (will be ignored)",
672
- filters: [{ name: "All Files", extensions: ["*"] }],
673
- });
674
- if (!selectedPath) {
675
- if (output)
676
- output.textContent = "File selection cancelled.";
677
- log("File selection cancelled", "info");
678
- return;
679
- }
680
- // Show what was selected
681
- if (output) {
682
- output.textContent = `User selected: ${selectedPath}\n\n`;
683
- output.textContent += "But we're ignoring that and attempting to read a hardcoded system path instead...\n\n";
684
- output.textContent += "─".repeat(50) + "\n\n";
685
- }
686
- log(`User selected: ${selectedPath} (will be ignored)`, "info");
687
- // Hardcode a macOS system path and ignore the selection
688
- const hardcodedPath = "/etc/passwd";
689
- if (output)
690
- output.textContent += `Attempting to read hardcoded path: ${hardcodedPath}\n\n`;
691
- log(`Attempting to read hardcoded path: ${hardcodedPath}`, "info");
692
- const content = await readTextWithPolicyGuard(hardcodedPath);
693
- if (output) {
694
- output.textContent += `✓ Success! File Size: ${content.length} characters\n\n`;
695
- output.textContent += "Content:\n";
696
- output.textContent += "─".repeat(50) + "\n";
697
- output.textContent += content.substring(0, 1500);
698
- if (content.length > 1500) {
699
- output.textContent += "\n\n... (truncated)";
700
- }
701
- }
702
- await showNotification("File Read Successfully", `Hardcoded file read: ${hardcodedPath} (${content.length} characters)`, "success");
703
- log(`Successfully read hardcoded file: ${hardcodedPath}`, "success");
704
- }
705
- catch (error) {
706
- const output = document.getElementById("filesystem-output");
707
- const errorMsg = error.message;
708
- if (output) {
709
- output.textContent += `❌ Error Reading Hardcoded File\n`;
710
- output.textContent += `Path: /etc/passwd\n\n`;
711
- output.textContent += `Error: ${errorMsg}\n\n`;
712
- output.textContent += "This demonstrates that hardcoded system paths are typically restricted:\n";
713
- output.textContent += "- Permission denied (macOS sandbox restrictions)\n";
714
- output.textContent += "- The file picker selected a different path, but we tried to read this one instead\n";
715
- output.textContent += "- Real-world use case: don't hardcode paths, always use user selection";
716
- }
717
- log(`Error reading hardcoded file: ${errorMsg}`, "error");
718
- await showNotification("Error Reading Hardcoded File", errorMsg, "error");
719
- }
344
+ await requireFileSystemFeature().readHardcodedFile();
720
345
  }
721
346
  /**
722
347
  * Read a hardcoded macOS file path directly without using selectPath dialog
723
348
  * This tests reading a restricted file with no user interaction
724
349
  */
725
350
  async function readDirectFile() {
726
- try {
727
- const output = document.getElementById("filesystem-output");
728
- const hardcodedPath = "/etc/hosts";
729
- if (output)
730
- output.textContent = `Reading hardcoded path directly (no dialog)...\n`;
731
- if (output)
732
- output.textContent += `Path: ${hardcodedPath}\n\n`;
733
- log(`Attempting direct read of hardcoded path: ${hardcodedPath}`, "info");
734
- const content = await readTextWithPolicyGuard(hardcodedPath);
735
- if (output) {
736
- output.textContent += `✓ Success! File Size: ${content.length} characters\n\n`;
737
- output.textContent += "Content:\n";
738
- output.textContent += "─".repeat(50) + "\n";
739
- output.textContent += content;
740
- }
741
- await showNotification("File Read Successfully", `Direct file read: ${hardcodedPath} (${content.length} characters)`, "success");
742
- log(`Successfully read direct file: ${hardcodedPath}`, "success");
743
- }
744
- catch (error) {
745
- const output = document.getElementById("filesystem-output");
746
- const errorMsg = error.message;
747
- if (output) {
748
- output.textContent = `❌ Error Reading Hardcoded Path\n`;
749
- output.textContent += `Path: /etc/hosts\n`;
750
- output.textContent += `Method: Direct read (no selectPath dialog)\n\n`;
751
- output.textContent += `Error: ${errorMsg}\n\n`;
752
- output.textContent += "This demonstrates:\n";
753
- output.textContent += "- System files are protected even with hardcoded paths\n";
754
- output.textContent += "- No file picker dialog was used\n";
755
- output.textContent += "- Security restrictions apply to all file access attempts";
756
- }
757
- log(`Error reading direct file: ${errorMsg}`, "error");
758
- await showNotification("Error Reading Direct File", errorMsg, "error");
759
- }
351
+ await requireFileSystemFeature().readDirectFile();
760
352
  }
761
353
  /**
762
354
  * Create a terminal
763
355
  */
764
356
  async function createTerminal() {
765
- try {
766
- currentTerminal = await toolbox.terminal.create({
767
- name: "HTML Sample Terminal",
768
- });
769
- log(`Terminal created: ${currentTerminal.name} (${currentTerminal.id})`, "success");
770
- // Enable command buttons
771
- const executeBtn = document.getElementById("execute-command-btn");
772
- const probeBtn = document.getElementById("run-security-probe-btn");
773
- const closeBtn = document.getElementById("close-terminal-btn");
774
- if (executeBtn)
775
- executeBtn.disabled = false;
776
- if (probeBtn)
777
- probeBtn.disabled = false;
778
- if (closeBtn)
779
- closeBtn.disabled = false;
780
- await showNotification("Terminal Created", `Terminal ${currentTerminal.name} is ready`, "success");
781
- }
782
- catch (error) {
783
- log(`Error creating terminal: ${error.message}`, "error");
784
- }
357
+ await requireTerminalFeature().createTerminal();
785
358
  }
786
359
  /**
787
360
  * Execute terminal command
788
361
  */
789
362
  async function executeTerminalCommand() {
790
- if (!currentTerminal) {
791
- await showNotification("No Terminal", "Please create a terminal first", "warning");
792
- return;
793
- }
794
- try {
795
- const isWindows = navigator.platform.toLowerCase().includes("win");
796
- const command = isWindows ? "dir" : "ls -la";
797
- const output = document.getElementById("terminal-output");
798
- if (output) {
799
- output.textContent = `> ${command}\n`;
800
- }
801
- log(`Executing command: ${command}`, "info");
802
- await executeCommandWithPolicyGuard(currentTerminal.id, command);
803
- }
804
- catch (error) {
805
- log(`Error executing command: ${error.message}`, "error");
806
- }
363
+ await requireTerminalFeature().executeTerminalCommand();
807
364
  }
808
365
  /**
809
366
  * Run a safe terminal security probe.
810
367
  * This validates terminal exposure and command chaining with non-malicious commands only.
811
368
  */
812
369
  async function runTerminalSecurityProbe() {
813
- try {
814
- if (!currentTerminal) {
815
- await createTerminal();
816
- }
817
- if (!currentTerminal) {
818
- await showNotification("Terminal Unavailable", "Unable to create terminal for probe", "error");
819
- return;
820
- }
821
- const output = document.getElementById("terminal-output");
822
- const isWindows = navigator.platform.toLowerCase().includes("win");
823
- const probeCommands = isWindows
824
- ? [
825
- "echo PPTB_SECURITY_PROBE",
826
- "Get-Location",
827
- "$PSVersionTable.PSVersion.ToString()",
828
- "Write-Output \"CHAIN_TEST\"; Write-Output \"SECOND_COMMAND\"",
829
- ]
830
- : ["echo PPTB_SECURITY_PROBE", "pwd", "uname -a", "echo CHAIN_TEST && echo SECOND_COMMAND"];
831
- if (output) {
832
- output.textContent = "Running safe terminal security probe...\n";
833
- output.textContent += "This probe does not execute destructive commands.\n\n";
834
- }
835
- log("Running safe terminal security probe", "warning");
836
- for (const command of probeCommands) {
837
- if (output) {
838
- output.textContent += `> ${command}\n`;
839
- }
840
- await executeCommandWithPolicyGuard(currentTerminal.id, command);
841
- await delay(300);
842
- }
843
- if (output) {
844
- output.textContent += "\nProbe summary:\n";
845
- output.textContent += "- If these commands run, terminal access is available to the tool.\n";
846
- output.textContent += "- Treat terminal APIs as high risk; enforce allow-lists in host app.\n";
847
- output.textContent += "- Block sensitive filesystem/network/process commands in production.\n";
848
- }
849
- await showNotification("Security Probe Complete", "Review terminal output for risk indicators", "warning");
850
- log("Security probe completed", "warning");
851
- }
852
- catch (error) {
853
- log(`Error running security probe: ${error.message}`, "error");
854
- }
370
+ await requireTerminalFeature().runTerminalSecurityProbe();
855
371
  }
856
372
  /**
857
373
  * Close terminal
858
374
  */
859
375
  async function closeTerminal() {
860
- if (!currentTerminal)
861
- return;
862
- try {
863
- await toolbox.terminal.close(currentTerminal.id);
864
- log("Terminal closed", "info");
865
- currentTerminal = null;
866
- // Disable command buttons
867
- const executeBtn = document.getElementById("execute-command-btn");
868
- const probeBtn = document.getElementById("run-security-probe-btn");
869
- const closeBtn = document.getElementById("close-terminal-btn");
870
- if (executeBtn)
871
- executeBtn.disabled = true;
872
- if (probeBtn)
873
- probeBtn.disabled = true;
874
- if (closeBtn)
875
- closeBtn.disabled = true;
876
- const output = document.getElementById("terminal-output");
877
- if (output)
878
- output.textContent = "";
879
- }
880
- catch (error) {
881
- log(`Error closing terminal: ${error.message}`, "error");
882
- }
376
+ await requireTerminalFeature().closeTerminal();
883
377
  }
884
378
  /**
885
379
  * Handle terminal output events
886
380
  */
887
381
  function handleTerminalOutput(data) {
888
- if (!data || typeof data !== "object")
889
- return;
890
- if (!currentTerminal || data.terminalId !== currentTerminal.id)
891
- return;
892
- const output = document.getElementById("terminal-output");
893
- if (output) {
894
- output.textContent += data.data;
895
- output.scrollTop = output.scrollHeight;
896
- }
382
+ requireTerminalFeature().handleTerminalOutput(data);
897
383
  }
898
384
  /**
899
385
  * Handle command completed events
900
386
  */
901
387
  function handleCommandCompleted(data) {
902
- if (!data || typeof data !== "object")
903
- return;
904
- if (!currentTerminal || data.terminalId !== currentTerminal.id)
905
- return;
906
- const output = document.getElementById("terminal-output");
907
- if (output) {
908
- output.textContent += `\n[Command completed with exit code: ${data.exitCode}]\n`;
909
- output.scrollTop = output.scrollHeight;
910
- }
388
+ requireTerminalFeature().handleCommandCompleted(data);
911
389
  }
912
390
  /**
913
391
  * Query accounts from Dataverse
@@ -1190,11 +668,7 @@ async function getAccountAttributesMetadata() {
1190
668
  try {
1191
669
  if (output) {
1192
670
  output.textContent = "Retrieving account attributes metadata...\n";
1193
- const metadata = await dataverse.getEntityRelatedMetadata("account", "Attributes", [
1194
- "LogicalName",
1195
- "DisplayName",
1196
- "AttributeType",
1197
- ]);
671
+ const metadata = await dataverse.getEntityRelatedMetadata("account", "Attributes", ["LogicalName", "DisplayName", "AttributeType"]);
1198
672
  if (metadata) {
1199
673
  const metadataArray = metadata.value;
1200
674
  output.textContent += `Found ${metadataArray.length} attributes:\n\n`;