Skip to content
Open
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
9 changes: 8 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"psr-4": {
"Pantheon\\Terminus\\Tests\\Functional\\": "tests/Functional/",
"Pantheon\\Terminus\\FeatureTests\\": "tests/features/bootstrap/",
"Pantheon\\Terminus\\Scripts\\": "scripts/"
"Pantheon\\Terminus\\Scripts\\": "scripts/",
"Pantheon\\Terminus\\UnitTests\\": "tests/unit_tests/"
},
"classmap": [
"scripts/UpdateClassLists.php"
Expand Down Expand Up @@ -110,6 +111,12 @@
"test:behat": [
"SHELL_INTERACTIVE=true TERMINUS_TEST_MODE=1 behat --colors --config tests/config/behat.yml --stop-on-failure --suite=default"
],
"test:unit": [
"vendor/bin/phpunit --colors=always -c ./phpunit.unit.xml --debug --do-not-cache-result --verbose --stop-on-failure"
],
"tests:unit": [
"@test:unit"
],
"test:short": [
"XDEBUG_MODE=coverage vendor/bin/phpunit --colors=always -c ./phpunit.xml --debug --group=short --do-not-cache-result --verbose --stop-on-failure"
],
Expand Down
15 changes: 15 additions & 0 deletions phpunit.unit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/unit_tests/bootstrap.php"
colors="true">
<testsuites>
<testsuite name="unit">
<directory suffix="Test.php">tests/unit_tests/</directory>
</testsuite>
</testsuites>
<php>
<server name="TERMINUS_CACHE_DIR" value="${TERMINUS_CACHE_DIR}" />
</php>
</phpunit>

84 changes: 84 additions & 0 deletions src/Helpers/LocalMachineHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,93 @@ protected function getProcess(string $cmd)
$config = $this->getConfig();
$process->setTimeout($config->get('timeout'));

// Forward environment variables to the subprocess.
// This allows users to control Composer behavior (e.g., COMPOSER_AUDIT_BLOCK_INSECURE)
// and other subprocess behavior via TERMINUS_FORWARD_ENV.
$forwardedVars = $this->getForwardedEnvironment();
if (!empty($forwardedVars)) {
// Merge forwarded vars with the current process environment.
// We need to get all environment variables and merge with forwarded ones.
// Use getenv() to get actual environment variables (not all $_SERVER keys are env vars).
$currentEnv = [];
// Get all environment variables using getenv() for each known env var from $_SERVER
// This ensures we only get actual environment variables, not other $_SERVER keys.
foreach ($_SERVER as $key => $value) {
if (is_string($key) && is_string($value)) {
// Verify this is actually an environment variable by checking getenv()
$envValue = getenv($key);
if ($envValue !== false) {
$currentEnv[$key] = $envValue;
}
}
}
// Also check $_ENV for any additional variables
foreach ($_ENV as $key => $value) {
if (is_string($key) && is_string($value) && !isset($currentEnv[$key])) {
$currentEnv[$key] = $value;
}
}
// Merge: start with current env, then override with forwarded vars
$env = array_merge($currentEnv, $forwardedVars);
$process->setEnv($env);
}

return $process;
}

/**
* Gets the environment variables that should be forwarded to subprocesses.
*
* This method:
* - Always forwards Composer-related environment variables (e.g., COMPOSER_AUDIT_BLOCK_INSECURE)
* to allow users to control Composer's security audit behavior during plugin installation.
* - Forwards any variables specified in TERMINUS_FORWARD_ENV (comma-separated list).
*
* Note: We do not globally disable Composer audits. We only respect explicit user
* instructions via environment variables.
*
* @return array Environment variables to forward (key => value pairs), or empty array if none to forward
*/
protected function getForwardedEnvironment(): array
{
$forwardedVars = [];

// Always forward Composer-related environment variables if they are set.
// This allows users to override Composer's security audit behavior.
$composerEnvVars = [
'COMPOSER_AUDIT_BLOCK_INSECURE',
'COMPOSER_ALLOW_SUPERUSER',
'COMPOSER_DISABLE_XDEBUG_WARN',
'COMPOSER_MEMORY_LIMIT',
'COMPOSER_MIRROR_PATH_REPOS',
'COMPOSER_NO_INTERACTION',
'COMPOSER_PROCESS_TIMEOUT',
];

foreach ($composerEnvVars as $var) {
$value = getenv($var);
if ($value !== false) {
$forwardedVars[$var] = $value;
}
}

// Check for TERMINUS_FORWARD_ENV (comma-separated list of env var names to forward).
$terminusForwardEnv = getenv('TERMINUS_FORWARD_ENV');
if ($terminusForwardEnv !== false && !empty($terminusForwardEnv)) {
$varsToForward = array_map('trim', explode(',', $terminusForwardEnv));
foreach ($varsToForward as $varName) {
if (!empty($varName)) {
$value = getenv($varName);
if ($value !== false) {
$forwardedVars[$varName] = $value;
}
}
}
}

return $forwardedVars;
}

/**
* Clones the Git repository.
*
Expand Down
80 changes: 80 additions & 0 deletions tests/Functional/PluginManagerCommandsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,86 @@ public function testPluginsCommands()
$filesystem->remove($tempPluginFile);
}

/**
* @test
* @covers \Pantheon\Terminus\Commands\Self\Plugin\InstallCommand
*
* Regression test to verify that COMPOSER_AUDIT_BLOCK_INSECURE environment variable
* is properly forwarded to Composer subprocess during plugin installation.
*
* This test verifies the fix for the bug where COMPOSER_AUDIT_BLOCK_INSECURE=0
* was not being forwarded to Composer, causing plugin installations to fail
* with security advisory errors even when the user explicitly opted out.
*
* Uses terminus-build-tools-plugin which has known security advisories in its
* dependencies (e.g., symfony/process v5.4.40, twig/twig v3.11.1) to verify
* that the env var forwarding actually works end-to-end.
*
* @group plugins
* @group long
*/
public function testPluginInstallRespectsComposerAuditBlockInsecureEnv()
{
$filesystem = new Filesystem();
// Use a plugin with known security advisories to test the env forwarding
$testPluginPackage = 'pantheon-systems/terminus-build-tools-plugin';

// Clean up.
$filesystem->remove([
$this->getPluginsDir(),
$this->getPlugins2Dir(),
$this->getDependenciesBaseDir(),
$this->getBaseDir(),
]);

// Uninstall plugin if it exists
$this->terminus('self:plugin:uninstall ' . $testPluginPackage, [], false);

// Test that plugin installation works with COMPOSER_AUDIT_BLOCK_INSECURE=0 set.
// This verifies that the environment variable is properly forwarded to Composer.
// Without this env var, Composer 2.9.1+ would block the installation due to
// security advisories in the plugin's dependencies.
$env = array_merge($this->env, ['COMPOSER_AUDIT_BLOCK_INSECURE' => '0']);
[$output, $exitCode, $error] = static::callTerminus(
sprintf('self:plugin:install %s', $testPluginPackage),
null,
$env
);

// The installation should succeed (exit code 0) when COMPOSER_AUDIT_BLOCK_INSECURE=0 is set.
// If the env var is not forwarded, Composer would fail with security advisory errors.
$this->assertEquals(
0,
$exitCode,
sprintf(
'Plugin installation should succeed with COMPOSER_AUDIT_BLOCK_INSECURE=0. ' .
'If this fails, the env var may not be forwarded to Composer. ' .
'Output: %s, Error: %s',
$output,
$error
)
);

// Verify the plugin was actually installed
$this->assertStringContainsString(
sprintf('Installed %s', $testPluginPackage),
$output . $error,
'Plugin installation output should indicate success.'
);

// Verify no security advisory blocking errors in the output
$combinedOutput = $output . $error;
$this->assertStringNotContainsString(
'are affected by security advisories',
$combinedOutput,
'Plugin installation should not show security advisory blocking errors '
. 'when COMPOSER_AUDIT_BLOCK_INSECURE=0 is set.'
);

// Cleanup
$this->terminus('self:plugin:uninstall ' . $testPluginPackage, [], false);
}

/**
* Install Terminus 2 plugins.
*
Expand Down
Loading
Loading