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/README.md +19 -14
- package/dist/app.js +71 -597
- package/dist/features/filesystem.js +291 -0
- package/dist/features/terminal.js +141 -0
- package/dist/index.html +172 -165
- 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 +2 -2
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`;
|