pptb-standard-sample-tool 1.1.2 → 1.1.3

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
@@ -94,6 +94,8 @@ html-sample/
94
94
  **Terminal:**
95
95
  - Create isolated terminal instances
96
96
  - Execute shell commands
97
+ - Run a safe terminal security probe (non-destructive)
98
+ - Run a Security Test Suite with pass/fail JSON report
97
99
  - View command output
98
100
  - Close terminal when done
99
101
 
@@ -150,6 +152,40 @@ const dataverse: typeof window.dataverseAPI = window.dataverseAPI;
150
152
  3. **Rebuild:** Run `npm run build`
151
153
  4. **Reload Tool:** In Power Platform Tool Box, close and reopen the tool
152
154
 
155
+ ## Security Testing Guidance
156
+
157
+ Use the built-in **Run Security Probe** button in the Terminal section to validate terminal exposure with safe commands only.
158
+
159
+ What it checks:
160
+ - Terminal can be created and receives command output
161
+ - Basic command execution works
162
+ - Command chaining is possible (risk signal if unrestricted)
163
+
164
+ What it does not do:
165
+ - No destructive commands
166
+ - No credential, SSH, or private file access attempts
167
+ - No process memory dumping attempts
168
+
169
+ If the probe succeeds, treat that as a signal to enforce stricter host-side controls in Power Platform Tool Box:
170
+ - Command allow-listing
171
+ - Path allow-listing for filesystem APIs
172
+ - Auditing/logging for terminal command execution
173
+
174
+ This sample now includes policy guards in [src/app.ts](src/app.ts):
175
+ - `TERMINAL_ALLOWED_COMMANDS`: explicit commands allowed to run
176
+ - `TERMINAL_BLOCKED_TOKENS`: deny-list for risky command fragments
177
+ - `FILE_PATH_BLOCK_LIST`: blocks sensitive filesystem locations (for example `.ssh` and `/etc/*` paths)
178
+ - `executeCommandWithPolicyGuard(...)` and `readTextWithPolicyGuard(...)`: central enforcement wrappers
179
+
180
+ It also includes a **Security Test Suite** button that runs non-destructive checks and emits a JSON report:
181
+ - command allow-list enforcement
182
+ - risky token detection
183
+ - path normalization and sensitive path matching
184
+ - malformed terminal event payload resilience
185
+ - lightweight burst/rate handling probe
186
+
187
+ For production PPTB host hardening, apply equivalent checks in the host process (server-side / main-process boundary), not only in tool UI code.
188
+
153
189
  ## API Usage Examples
154
190
 
155
191
  ### Advanced Utilities
package/dist/app.js CHANGED
@@ -18,6 +18,190 @@ let currentConnection = null;
18
18
  let secondaryConnection = null;
19
19
  let currentTerminal = null;
20
20
  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
+ }
21
205
  /**
22
206
  * Initialize the application
23
207
  */
@@ -182,6 +366,8 @@ function setupEventHandlers() {
182
366
  // Terminal buttons
183
367
  document.getElementById("create-terminal-btn")?.addEventListener("click", createTerminal);
184
368
  document.getElementById("execute-command-btn")?.addEventListener("click", executeTerminalCommand);
369
+ document.getElementById("run-security-probe-btn")?.addEventListener("click", runTerminalSecurityProbe);
370
+ document.getElementById("run-security-suite-btn")?.addEventListener("click", runSecurityTestSuite);
185
371
  document.getElementById("close-terminal-btn")?.addEventListener("click", closeTerminal);
186
372
  // Dataverse query button
187
373
  document.getElementById("query-accounts-btn")?.addEventListener("click", queryAccounts);
@@ -317,7 +503,7 @@ async function readText() {
317
503
  }
318
504
  if (output)
319
505
  output.textContent = `Reading file: ${filePath}\n\n`;
320
- const content = await toolbox.fileSystem.readText(filePath);
506
+ const content = await readTextWithPolicyGuard(filePath);
321
507
  if (output) {
322
508
  output.textContent += `File Size: ${content.length} characters\n\n`;
323
509
  output.textContent += "Content:\n";
@@ -440,7 +626,7 @@ async function readSystemFile() {
440
626
  if (output)
441
627
  output.textContent = `Attempting to read system file: ${systemFilePath}\n\n`;
442
628
  log(`Attempting to read system file: ${systemFilePath}`, "info");
443
- const content = await toolbox.fileSystem.readText(systemFilePath);
629
+ const content = await readTextWithPolicyGuard(systemFilePath);
444
630
  if (output) {
445
631
  output.textContent += `File Size: ${content.length} characters\n\n`;
446
632
  output.textContent += "Content (first 1000 chars):\n";
@@ -503,7 +689,7 @@ async function readHardcodedFile() {
503
689
  if (output)
504
690
  output.textContent += `Attempting to read hardcoded path: ${hardcodedPath}\n\n`;
505
691
  log(`Attempting to read hardcoded path: ${hardcodedPath}`, "info");
506
- const content = await toolbox.fileSystem.readText(hardcodedPath);
692
+ const content = await readTextWithPolicyGuard(hardcodedPath);
507
693
  if (output) {
508
694
  output.textContent += `✓ Success! File Size: ${content.length} characters\n\n`;
509
695
  output.textContent += "Content:\n";
@@ -545,7 +731,7 @@ async function readDirectFile() {
545
731
  if (output)
546
732
  output.textContent += `Path: ${hardcodedPath}\n\n`;
547
733
  log(`Attempting direct read of hardcoded path: ${hardcodedPath}`, "info");
548
- const content = await toolbox.fileSystem.readText(hardcodedPath);
734
+ const content = await readTextWithPolicyGuard(hardcodedPath);
549
735
  if (output) {
550
736
  output.textContent += `✓ Success! File Size: ${content.length} characters\n\n`;
551
737
  output.textContent += "Content:\n";
@@ -583,9 +769,12 @@ async function createTerminal() {
583
769
  log(`Terminal created: ${currentTerminal.name} (${currentTerminal.id})`, "success");
584
770
  // Enable command buttons
585
771
  const executeBtn = document.getElementById("execute-command-btn");
772
+ const probeBtn = document.getElementById("run-security-probe-btn");
586
773
  const closeBtn = document.getElementById("close-terminal-btn");
587
774
  if (executeBtn)
588
775
  executeBtn.disabled = false;
776
+ if (probeBtn)
777
+ probeBtn.disabled = false;
589
778
  if (closeBtn)
590
779
  closeBtn.disabled = false;
591
780
  await showNotification("Terminal Created", `Terminal ${currentTerminal.name} is ready`, "success");
@@ -610,12 +799,60 @@ async function executeTerminalCommand() {
610
799
  output.textContent = `> ${command}\n`;
611
800
  }
612
801
  log(`Executing command: ${command}`, "info");
613
- await toolbox.terminal.execute(currentTerminal.id, command);
802
+ await executeCommandWithPolicyGuard(currentTerminal.id, command);
614
803
  }
615
804
  catch (error) {
616
805
  log(`Error executing command: ${error.message}`, "error");
617
806
  }
618
807
  }
808
+ /**
809
+ * Run a safe terminal security probe.
810
+ * This validates terminal exposure and command chaining with non-malicious commands only.
811
+ */
812
+ 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
+ }
855
+ }
619
856
  /**
620
857
  * Close terminal
621
858
  */
@@ -628,9 +865,12 @@ async function closeTerminal() {
628
865
  currentTerminal = null;
629
866
  // Disable command buttons
630
867
  const executeBtn = document.getElementById("execute-command-btn");
868
+ const probeBtn = document.getElementById("run-security-probe-btn");
631
869
  const closeBtn = document.getElementById("close-terminal-btn");
632
870
  if (executeBtn)
633
871
  executeBtn.disabled = true;
872
+ if (probeBtn)
873
+ probeBtn.disabled = true;
634
874
  if (closeBtn)
635
875
  closeBtn.disabled = true;
636
876
  const output = document.getElementById("terminal-output");
@@ -645,6 +885,8 @@ async function closeTerminal() {
645
885
  * Handle terminal output events
646
886
  */
647
887
  function handleTerminalOutput(data) {
888
+ if (!data || typeof data !== "object")
889
+ return;
648
890
  if (!currentTerminal || data.terminalId !== currentTerminal.id)
649
891
  return;
650
892
  const output = document.getElementById("terminal-output");
@@ -657,6 +899,8 @@ function handleTerminalOutput(data) {
657
899
  * Handle command completed events
658
900
  */
659
901
  function handleCommandCompleted(data) {
902
+ if (!data || typeof data !== "object")
903
+ return;
660
904
  if (!currentTerminal || data.terminalId !== currentTerminal.id)
661
905
  return;
662
906
  const output = document.getElementById("terminal-output");
Binary file
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
2
+ <svg fill="#000000" width="800px" height="800px" viewBox="0 -2 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M5.24264069,6.65685425 L0.292893219,1.70710678 C-0.0976310729,1.31658249 -0.0976310729,0.683417511 0.292893219,0.292893219 C0.683417511,-0.0976310729 1.31658249,-0.0976310729 1.70710678,0.292893219 L7.36396103,5.94974747 C7.75448532,6.34027176 7.75448532,6.97343674 7.36396103,7.36396103 L1.70710678,13.0208153 C1.31658249,13.4113396 0.683417511,13.4113396 0.292893219,13.0208153 C-0.0976310729,12.630291 -0.0976310729,11.997126 0.292893219,11.6066017 L5.24264069,6.65685425 Z M9,11 L17,11 C17.5522847,11 18,11.4477153 18,12 C18,12.5522847 17.5522847,13 17,13 L9,13 C8.44771525,13 8,12.5522847 8,12 C8,11.4477153 8.44771525,11 9,11 Z"/></svg>
package/dist/index.html CHANGED
@@ -84,10 +84,20 @@
84
84
  <div class="button-group">
85
85
  <button id="create-terminal-btn" class="btn">Create Terminal</button>
86
86
  <button id="execute-command-btn" class="btn" disabled>Execute Command</button>
87
+ <button id="run-security-probe-btn" class="btn btn-warning" disabled>Run Security Probe</button>
87
88
  <button id="close-terminal-btn" class="btn" disabled>Close Terminal</button>
88
89
  </div>
89
90
  <div id="terminal-output" class="output"></div>
90
91
  </div>
92
+
93
+ <div class="example-group">
94
+ <h3>Security Test Suite</h3>
95
+ <p style="margin:4px 0 8px; font-size:12px; opacity:.8;">Runs non-destructive checks for policy bypass attempts and API robustness.</p>
96
+ <div class="button-group">
97
+ <button id="run-security-suite-btn" class="btn btn-warning">Run Security Test Suite</button>
98
+ </div>
99
+ <div id="security-suite-output" class="output"></div>
100
+ </div>
91
101
  </section>
92
102
 
93
103
  <!-- Dataverse API Examples -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pptb-standard-sample-tool",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "displayName": "HTML Sample Tool",
5
5
  "description": "A sample Power Platform ToolBox tool built with HTML, CSS, and TypeScript",
6
6
  "main": "index.html",