Skip to content

Commit 1033287

Browse files
committed
Merge branch 'release/0.8.10'
2 parents f65d938 + 09afcad commit 1033287

9 files changed

Lines changed: 1448 additions & 4 deletions

File tree

.version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"strategy": "semver",
33
"major": 0,
44
"minor": 8,
5-
"patch": 9,
5+
"patch": 10,
66
"build": 0
77
}

composer.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
"minimum-stability": "stable",
1313
"require": {
1414
"php": "^8.4",
15-
"neuron-php/application": "0.8.*"
15+
"neuron-php/application": "0.8.*",
16+
"neuron-php/data": "0.9.*",
17+
"symfony/yaml": "^6.4"
1618
},
1719
"require-dev": {
1820
"phpunit/phpunit": "^9.0",
@@ -25,8 +27,7 @@
2527
},
2628
"autoload-dev": {
2729
"psr-4": {
28-
"Tests\\": "tests/",
29-
"Neuron\\Core\\": "../core/src/Core/"
30+
"Tests\\": "tests/"
3031
}
3132
},
3233
"bin": [
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
namespace Neuron\Cli\Commands\Secrets;
4+
5+
use Neuron\Cli\Commands\Command;
6+
use Neuron\Data\Settings\SecretManager;
7+
8+
/**
9+
* Edit encrypted secrets command
10+
*
11+
* Opens encrypted credentials in an editor for secure editing.
12+
* Automatically re-encrypts the file when the editor is closed.
13+
*
14+
* Usage:
15+
* neuron secrets:edit # Edit default secrets
16+
* neuron secrets:edit --env=production # Edit production secrets
17+
* neuron secrets:edit --editor="code --wait" # Use VS Code
18+
*
19+
* @package Neuron\Cli\Commands\Secrets
20+
*/
21+
class EditCommand extends Command
22+
{
23+
private SecretManager $secretManager;
24+
25+
/**
26+
* @inheritDoc
27+
*/
28+
public function getName(): string
29+
{
30+
return 'secrets:edit';
31+
}
32+
33+
/**
34+
* @inheritDoc
35+
*/
36+
public function getDescription(): string
37+
{
38+
return 'Edit encrypted secrets file';
39+
}
40+
41+
/**
42+
* @inheritDoc
43+
*/
44+
public function configure(): void
45+
{
46+
$this->addOption( 'env', 'e', true, 'Environment to edit (default: base secrets)' );
47+
$this->addOption( 'editor', null, true, 'Editor to use (default: vi)' );
48+
$this->addOption( 'config', 'c', true, 'Config directory path (default: config)' );
49+
$this->addOption( 'verbose', 'v', false, 'Verbose output' );
50+
}
51+
52+
/**
53+
* @inheritDoc
54+
*/
55+
public function execute(): int
56+
{
57+
$configPath = $this->input->getOption( 'config', 'config' );
58+
$env = $this->input->getOption( 'env' );
59+
60+
// Handle editor option - could be null, true (flag without value), or a string
61+
$editorOption = $this->input->getOption( 'editor' );
62+
if( is_string( $editorOption ) && $editorOption !== '' )
63+
{
64+
$editor = $editorOption;
65+
}
66+
else
67+
{
68+
$editor = getenv( 'EDITOR' ) ?: 'vi';
69+
}
70+
71+
// Determine paths based on environment
72+
if( $env )
73+
{
74+
$credentialsPath = $configPath . '/secrets/' . $env . '.yml.enc';
75+
$keyPath = $configPath . '/secrets/' . $env . '.key';
76+
$this->output->info( "Editing {$env} environment secrets..." );
77+
}
78+
else
79+
{
80+
$credentialsPath = $configPath . '/secrets.yml.enc';
81+
$keyPath = $configPath . '/master.key';
82+
$this->output->info( "Editing base secrets..." );
83+
}
84+
85+
// Create SecretManager
86+
$this->secretManager = new SecretManager();
87+
88+
try
89+
{
90+
// Ensure key exists
91+
if( !file_exists( $keyPath ) )
92+
{
93+
$this->output->warning( "Key file not found at: {$keyPath}" );
94+
$this->output->info( "Generating new encryption key..." );
95+
96+
// Ensure directory exists
97+
$dir = dirname( $keyPath );
98+
if( !is_dir( $dir ) )
99+
{
100+
if( !mkdir( $dir, 0755, true ) )
101+
{
102+
$this->output->error( "Failed to create directory: {$dir}" );
103+
return 1;
104+
}
105+
}
106+
107+
$this->secretManager->generateKey( $keyPath );
108+
$this->output->success( "Generated new key at: {$keyPath}" );
109+
$this->output->warning( "IMPORTANT: Add {$keyPath} to .gitignore!" );
110+
}
111+
112+
// Edit the secrets
113+
$result = $this->secretManager->edit( $credentialsPath, $keyPath, $editor );
114+
115+
if( $result )
116+
{
117+
$this->output->success( "Secrets saved to: {$credentialsPath}" );
118+
119+
// First time setup reminder - check for .gitignore in project root
120+
$projectRoot = dirname( $configPath );
121+
$gitignorePath = $projectRoot . '/.gitignore';
122+
if( !$env && !file_exists( $gitignorePath ) )
123+
{
124+
$this->output->newLine();
125+
$this->output->warning( "Remember to:" );
126+
$this->output->write( "1. Add {$keyPath} to .gitignore" );
127+
$this->output->write( "2. Commit {$credentialsPath} to version control" );
128+
$this->output->write( "3. Share {$keyPath} securely with your team" );
129+
}
130+
}
131+
else
132+
{
133+
$this->output->error( "Failed to save secrets" );
134+
return 1;
135+
}
136+
}
137+
catch( \Exception $e )
138+
{
139+
$this->output->error( "Error editing secrets: " . $e->getMessage() );
140+
141+
if( $this->input->hasOption( 'verbose' ) )
142+
{
143+
$this->output->write( $e->getTraceAsString() );
144+
}
145+
146+
return 1;
147+
}
148+
149+
return 0;
150+
}
151+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
namespace Neuron\Cli\Commands\Secrets\Key;
4+
5+
use Neuron\Cli\Commands\Command;
6+
use Neuron\Data\Settings\SecretManager;
7+
8+
/**
9+
* Generate encryption key command
10+
*
11+
* Generates a new encryption key for securing credentials.
12+
* Keys are cryptographically secure random values.
13+
*
14+
* Usage:
15+
* neuron secrets:key:generate # Generate master key
16+
* neuron secrets:key:generate --env=production # Generate production key
17+
* neuron secrets:key:generate --force # Overwrite existing key
18+
*
19+
* @package Neuron\Cli\Commands\Secrets\Key
20+
*/
21+
class GenerateCommand extends Command
22+
{
23+
private SecretManager $secretManager;
24+
25+
/**
26+
* @inheritDoc
27+
*/
28+
public function getName(): string
29+
{
30+
return 'secrets:key:generate';
31+
}
32+
33+
/**
34+
* @inheritDoc
35+
*/
36+
public function getDescription(): string
37+
{
38+
return 'Generate a new encryption key for secrets';
39+
}
40+
41+
/**
42+
* @inheritDoc
43+
*/
44+
public function configure(): void
45+
{
46+
$this->addOption( 'env', 'e', true, 'Environment for the key (default: master key)' );
47+
$this->addOption( 'config', 'c', true, 'Config directory path (default: config)' );
48+
$this->addOption( 'force', 'f', false, 'Overwrite existing key file' );
49+
$this->addOption( 'show', 's', false, 'Display the generated key' );
50+
$this->addOption( 'verbose', 'v', false, 'Verbose output' );
51+
}
52+
53+
/**
54+
* @inheritDoc
55+
*/
56+
public function execute(): int
57+
{
58+
$configPath = $this->input->getOption( 'config', 'config' );
59+
$env = $this->input->getOption( 'env' );
60+
$force = $this->input->hasOption( 'force' );
61+
$show = $this->input->hasOption( 'show' );
62+
63+
// Determine key path based on environment
64+
if( $env )
65+
{
66+
$keyPath = $configPath . '/secrets/' . $env . '.key';
67+
$keyName = $env . ' environment key';
68+
69+
// Ensure directory exists
70+
$dir = dirname( $keyPath );
71+
if( !is_dir( $dir ) )
72+
{
73+
if( !mkdir( $dir, 0755, true ) )
74+
{
75+
$this->output->error( "Failed to create directory: {$dir}" );
76+
return 1;
77+
}
78+
}
79+
}
80+
else
81+
{
82+
$keyPath = $configPath . '/master.key';
83+
$keyName = 'master key';
84+
85+
// Ensure directory exists
86+
$dir = dirname( $keyPath );
87+
if( !is_dir( $dir ) )
88+
{
89+
if( !mkdir( $dir, 0755, true ) )
90+
{
91+
$this->output->error( "Failed to create directory: {$dir}" );
92+
return 1;
93+
}
94+
}
95+
}
96+
97+
// Check if key already exists
98+
if( file_exists( $keyPath ) && !$force )
99+
{
100+
$this->output->error( "Key file already exists: {$keyPath}" );
101+
$this->output->info( "Use --force to overwrite the existing key." );
102+
$this->output->warning( "WARNING: Overwriting will make existing encrypted files unreadable!" );
103+
return 1;
104+
}
105+
106+
// Warn about overwriting
107+
if( file_exists( $keyPath ) && $force )
108+
{
109+
$this->output->warning( "You are about to overwrite an existing key!" );
110+
$this->output->warning( "This will make any files encrypted with the old key unreadable." );
111+
112+
if( !$this->confirm( "Are you absolutely sure you want to continue?" ) )
113+
{
114+
$this->output->info( "Operation cancelled." );
115+
return 0;
116+
}
117+
}
118+
119+
// Create SecretManager and generate key
120+
$this->secretManager = new SecretManager();
121+
122+
try
123+
{
124+
$key = $this->secretManager->generateKey( $keyPath, $force );
125+
126+
$this->output->success( "Generated {$keyName} at: {$keyPath}" );
127+
128+
// Show the key if requested
129+
if( $show )
130+
{
131+
$this->output->newLine();
132+
$this->output->section( "Generated Key" );
133+
$this->output->write( $key );
134+
$this->output->newLine();
135+
$this->output->warning( "This key is shown only once. Store it securely!" );
136+
}
137+
138+
// Display instructions
139+
$this->output->newLine();
140+
$this->output->info( "Next steps:" );
141+
$this->output->write( "1. Add {$keyPath} to .gitignore (NEVER commit this file)" );
142+
$this->output->write( "2. Share this key securely with your team" );
143+
$this->output->write( "3. Use 'neuron secrets:edit" . ($env ? " --env={$env}" : "") . "' to add secrets" );
144+
145+
// Environment variable alternative
146+
$envVar = 'NEURON_' . strtoupper(
147+
str_replace( ['/', '.', '-'], '_', basename( $keyPath, '.key' ) )
148+
) . '_KEY';
149+
$this->output->newLine();
150+
$this->output->info( "Alternative: Set the key as an environment variable:" );
151+
if( $show )
152+
{
153+
$this->output->write( "export {$envVar}={$key}" );
154+
}
155+
else
156+
{
157+
$this->output->write( "export {$envVar}=<KEY_FROM_{$keyPath}>" );
158+
}
159+
}
160+
catch( \Exception $e )
161+
{
162+
$this->output->error( "Error generating key: " . $e->getMessage() );
163+
164+
if( $this->input->hasOption( 'verbose' ) )
165+
{
166+
$this->output->write( $e->getTraceAsString() );
167+
}
168+
169+
return 1;
170+
}
171+
172+
return 0;
173+
}
174+
}

0 commit comments

Comments
 (0)