Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/validate-repository-metadata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ name: validate-repository-metadata

on:
pull_request:
branches:
- main
workflow_dispatch:

jobs:
validate-repository-metadata:
name: validate-repository-metadata
runs-on: ubuntu-latest
env:
POWER_PLATFORM_SKILLS_TELEMETRY_POWER_PAGES_OPTOUT: "1"
steps:
- name: checkout
uses: actions/checkout@v4
Expand All @@ -30,3 +30,9 @@ jobs:

- name: validate-telemetry-ikeys
run: node scripts/validate-telemetry-ikeys.js

- name: test-secure-process-execution-validator
run: node --test scripts/tests/validate-secure-process-execution.test.js

- name: validate-secure-process-execution
run: node scripts/validate-secure-process-execution.js
1 change: 1 addition & 0 deletions plugins/power-pages/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Read `PLUGIN_DEVELOPMENT_GUIDE.md` for UX and reliability standards when creatin
- **UUID generation** must use the shared `scripts/generate-uuid.js` — never copy it into skill-specific directories.
- **Power Pages config loading** must reuse `scripts/lib/powerpages-config.js` anywhere a script reads `.powerpages-site` table-permission or site-setting YAML. Keep that module focused on loading/parsing code-site config only; put validation or business rules in separate validator modules.
- **Script changes require tests** — Whenever you add a new script or modify an existing script, add or update `node:test` coverage under `scripts/tests/`. Prefer one `*.test.js` file per script/module being tested, and keep the test command passing: `node --test plugins/power-pages/scripts/tests/` (Node's built-in runner discovers `*.test.js` files under the given directory). Validator changes are not an exception; they must always ship with test coverage.
- **Secure process validation** — After changing Power Pages `child_process` usage, run `node scripts/validate-secure-process-execution.js` from the repository root. Its fixture suite is `node --test scripts/tests/validate-secure-process-execution.test.js`.
- **Dataverse-backed validation** must stay opt-in for local runs only. Do not require live Dataverse connectivity in CI workflows or default test runs; gate it behind explicit local flags such as `--validate-dataverse-relationships`.
- **Azure CLI `--allow-no-subscriptions`** — this flag is only valid on `az login`. Other `az` subcommands (`az account get-access-token`, `az account show`, etc.) reject it as an unrecognized argument and exit 2, so do NOT add it to anything other than `az login`. When the user is not logged in to the Azure CLI, suggest plain `az login` first; only suggest `az login --allow-no-subscriptions` as a fallback if they don't have any associated Azure subscription, since that variant lets subscription-less accounts sign in and still mint AAD-scoped Dataverse/Power Platform tokens via subsequent `az account get-access-token` calls. Reuse the shared `getAuthToken` helper in `scripts/lib/validation-helpers.js` instead of shelling out to `az` directly.
- **Reference docs** shared across skills live in `references/` — reference via `${PLUGIN_ROOT}/references/` paths, don't duplicate.
Expand Down
6 changes: 4 additions & 2 deletions plugins/power-pages/scripts/lib/detect-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// Returns a Playwright channel name ('msedge', 'chrome', 'chromium').
// Used by the Playwright MCP launcher and the axe-core audit script.

const { execSync } = require('child_process');
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
Expand All @@ -15,7 +15,9 @@ function exists(filePath) {

function whichExists(cmd) {
try {
execSync(`which ${cmd}`, { stdio: 'ignore' });
// This helper only runs in the Linux branch below. Pass the fixed utility and
// candidate name separately so a future candidate cannot become shell syntax.
execFileSync('which', [cmd], { stdio: 'ignore', shell: false });
return true;
} catch {
return false;
Expand Down
27 changes: 21 additions & 6 deletions plugins/power-pages/skills/scan-code/scripts/check-tools.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node

const { execSync } = require('child_process');
const { execFileSync } = require('child_process');

if (process.argv.includes('--help')) {
process.stdout.write(`check-tools.js — Detects whether opengrep and trivy are installed.
Expand All @@ -23,19 +23,34 @@ Output (stdout, JSON):
process.exit(0);
}

function probe(cmd, parseVersion) {
function probe(runVersion, parseVersion) {
try {
// 60s timeout — first invocation can be slow (cold start, antivirus scan, etc.)
const out = execSync(cmd, { encoding: 'utf8', timeout: 60000, stdio: ['ignore', 'pipe', 'pipe'] });
const out = runVersion();
return { available: true, version: parseVersion(out), error: null };
} catch (err) {
return { available: false, version: null, error: (err.stderr || err.message || '').toString().trim() };
}
}

// Keep executable names at the child_process call sites. Passing an executable
// into probe() would make future CLI/env-derived values indistinguishable from
// this fixed two-tool allowlist to the repository security validator.
const runOpenGrepVersion = () => execFileSync('opengrep', ['--version'], {
encoding: 'utf8',
timeout: 60000,
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
});
const runTrivyVersion = () => execFileSync('trivy', ['--version'], {
encoding: 'utf8',
timeout: 60000,
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
});

const result = {
opengrep: probe('opengrep --version', (out) => (out.match(/[\d.]+/) || [null])[0]),
trivy: probe('trivy --version', (out) => {
opengrep: probe(runOpenGrepVersion, (out) => (out.match(/[\d.]+/) || [null])[0]),
trivy: probe(runTrivyVersion, (out) => {
const m = out.match(/Version:\s*([\d.]+)/i) || out.match(/[\d.]+/);
return m ? m[1] || m[0] : null;
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

const { spawn } = require('child_process');

function launch(options) {
spawn('tool', [process.env.INPUT], options);
}

module.exports = { launch };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

require('child_process')['exec'](process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

const childProcess = require('child_process');
const method = process.env.METHOD;

childProcess[method]('tool', []);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { exec as run } from 'child_process';

run('pac pages upload --path ' + process.argv[2]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import childProcess from 'node:child_process';

childProcess.exec(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

require('node:child_process').exec('tool ' + process.argv[2]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

const x = 1;
const y = 2;

x++ / require('child_process').exec(process.env.COMMAND) / y;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const { spawn } = require('child_process');

spawn('tool', [], { shell: false, shell: true });
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const { spawnSync } = require('child_process');

spawnSync(process.env.TOOL, ['--version'], { shell: false });
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const { execSync: run } = require('node:child_process');

run(`pac pages upload --path ${process.env.SITE_PATH}`);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

require('child_\x70rocess').exec(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const { spawn } = require('child_process');

spawn('tool', [], { 'sh\u0065ll': true });
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

const { exec } = require('child_process');
const input = process.env.INPUT;

exec(`echo \\${input}`);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

const { execFile } = require('child_process');
const options = {
encoding: 'utf8',
shell: true,
};

execFile('tool', [process.env.INPUT], options);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const childProcess = require('child_process');

childProcess.execSync(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('child\137process').exec(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { spawn } = require('child_process');

spawn('tool', [], { shell: false, 'sh\145ll': true });
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const { exec } = require('child_process');

exec?.(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const childProcess = require('child_process');

childProcess['exec']?.(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const childProcess = require('child_process');

childProcess?.['exec']?.(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const childProcess = require('child_process');

childProcess.exec?.(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

const { exec } = require('child_process');
const command = 'pac env who';

function run(command) {
exec(command);
}

module.exports = { run };
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const { exec } = require('child_process');

(exec)(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const childProcess = require('child_process');

(childProcess.exec)(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

(require('child_process')).exec(process.env.COMMAND);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

const { execSync } = require('child_process');

function validate(checkScript, projectRoot) {
return execSync(`node "${checkScript}" --projectRoot "${projectRoot}"`, {
encoding: 'utf8',
timeout: 30000,
});
}

module.exports = { validate };
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const { spawn } = require('child_process');

spawn('tool', [], { 'shell': true });
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

const { exec } = require('child_process');
const command = process.env.COMMAND;
exec(command);

{
const command = 'pac env who';
void command;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const childProcess = require('node:child_process');

childProcess.spawn('tool', [process.argv[2]], { shell: true });
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

const { spawn } = require('child_process');
const inherited = { shell: true };

spawn('tool', [], { ...inherited });
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

const { spawn } = require('child_process');

function launch(options) {
spawn('tool', options);
}

module.exports = { launch };
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@sealed
class ToolRunner {}

module.exports = ToolRunner;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

const example = "execSync(`${process.env.INPUT}`)";
const matcher = /spawn\("tool", args, \{ shell: true \}\)/;

// execSync(`tool ${userInput}`);
module.exports = { example, matcher };
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

const { execSync: run } = require('child_process');

run(
'pac env who',
{
encoding: 'utf8',
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

const ok = true;
const value = 'example';

if (ok) /require("child_process").exec(process.env.X)/.test(value);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { execFileSync as runTool } from 'node:child_process';

const userPath = process.env.USER_PATH;
runTool(
'C:\\Program Files\\Contoso\\tool.exe',
['/tmp/site with spaces', 'C:\\sites\\demo & literal', userPath],
{ shell: false }
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';

const { execFileSync } = require('child_process');

function validate(checkScript, projectRoot) {
return execFileSync(process.execPath, [checkScript, '--projectRoot', projectRoot], {
encoding: 'utf8',
timeout: 30000,
shell: false,
});
}

module.exports = { validate };
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

const childProcess = require('node:child_process');
const { promisify } = require('node:util');

const run = promisify(childProcess.execFile);
const options = { encoding: 'utf8', shell: false };

run('pac', ['env', 'who'], options);
childProcess.spawnSync(process.execPath, ['script.js', process.env.INPUT], options);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const { exec } = require('child_process');

exec(`pac auth who`);
Loading
Loading