diff --git a/.gitignore b/.gitignore index 9c22a5a..29bb86d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ # ZeroEnv master key .secrets.key + +# Python build artifacts +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ diff --git a/src/zeroenv.egg-info/PKG-INFO b/src/zeroenv.egg-info/PKG-INFO new file mode 100644 index 0000000..40b1ab9 --- /dev/null +++ b/src/zeroenv.egg-info/PKG-INFO @@ -0,0 +1,534 @@ +Metadata-Version: 2.4 +Name: zeroenv +Version: 0.1.0 +Summary: Simple, secure, and git-safe secrets management.. +Author-email: "ropeadope62 (Dave C.)" +Project-URL: Homepage, https://github.com/ropeadope62/zeroenv +Project-URL: Bug Tracker, https://github.com/ropeadope62/zeroenv/issues +Classifier: Development Status :: 2 - Pre-Alpha +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Topic :: Security :: Cryptography +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Classifier: Environment :: Console +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: click +Requires-Dist: cryptography +Requires-Dist: rich +Dynamic: license-file + +

+
+ ZeroEnv +

+ +

Simple, secure, and git-safe secrets management.

+ +

+ + PyPI version + + + + + + + + + Tests + +

+ +

+ Features • + Installation • + Usage • + How It Works • + Team Workflow • + Docker • + Testing • + Security • + Command Reference +

+ +![demo](./assets/zeroenv_example.gif) + +## Features + +* AES-256-GCM encryption with unique nonces +* Git-safe encrypted storage (commit `.secrets`, not keys) +* Zero-config initialization +* No code changes required (injects env vars) +* No infrastructure dependencies +* Cross-platform (Windows, macOS, Linux) +* Export to `.env` format + +## Installation + +```bash +pip install zeroenv +``` + +From source: +```bash +git clone https://github.com/ropeadope62/zeroenv +cd zeroenv +pip install -e . +``` + +## Usage + +### Initialize + +```bash +zeroenv init # Standard tier (fast, development) +zeroenv init --tier enhanced # Enhanced security (100k iterations) +zeroenv init --tier max # Maximum security (500k iterations, production) +``` + +Creates `.secrets` (encrypted) and `.secrets.key` (master key). Automatically updates `.gitignore`. + +**Security Tiers:** +- **Standard**: Direct key usage, fastest performance (development) +- **Enhanced**: PBKDF2-SHA256 with 100k iterations (balanced security/performance) +- **Max**: PBKDF2-SHA256 with 500k iterations (production environments) + +### Add Secrets + +```bash +zeroenv add DATABASE_URL "postgresql://localhost/mydb" +zeroenv add API_KEY "sk_test_1234567890" +``` + +### Run Commands + +```bash +zeroenv run python app.py +zeroenv run npm start +zeroenv run dotnet run +``` + +Secrets are injected as environment variables. Your code remains unchanged: + +```python +import os +api_key = os.getenv('API_KEY') +``` + +### List Secrets + +```bash +zeroenv ls # Names only +zeroenv ls --values # Show values +``` + +### Get Secret + +```bash +zeroenv get API_KEY +``` + +### Export + +```bash +zeroenv export # .env format +zeroenv export > .env # Save to file +zeroenv export -f json # JSON format +``` + +### Remove Secret + +```bash +zeroenv rm SECRET_NAME +``` + +### View Configuration + +```bash +zeroenv info +``` + +Displays: +- Security tier and description +- PBKDF2 iteration count +- Encryption algorithm (AES-256-GCM) +- Number of secrets stored +- File locations + +## How It Works + +### Two-File System + +**`.secrets`** - Encrypted secrets (safe to commit) +```json +{ + "version": "1.0", + "security_tier": "enhanced", + "salt": "xY9zA1bC2dE3fG4h...", + "secrets": { + "API_KEY": { + "ciphertext": "aB3dE5fG7hI9jK1...", + "nonce": "pQ6rS8tU0vW1xY2z..." + } + } +} +``` + +**`.secrets.key`** - Master key (never commit, auto-gitignored) +``` +aB3dE5fG7hI9jK1lM2nO4pQ6rS8tU0vW1xY2zA3bC4dE5f= +``` + +### Encryption + +- **Algorithm:** AES-256-GCM +- **Key size:** 256 bits +- **Nonce size:** 96 bits (unique per secret) +- **Authenticated encryption:** Tamper-proof +- **Key derivation:** PBKDF2-HMAC-SHA256 (enhanced/max tiers) +- **KDF iterations:** 100k (enhanced) or 500k (max) + +The encryption process varies by security tier: +- **Standard:** Master key → AES-256-GCM encryption +- **Enhanced/Max:** Master key + salt → PBKDF2 → Derived key → AES-256-GCM encryption + +## Team Usage + +While ZeroEnv was designed with individual developers in mind, usage in small teams can be achieved by sharing the master key. + +### Setup + +```bash +# Developer 1 +zeroenv init +zeroenv add API_KEY "shared-key" +git add .secrets .gitignore +git commit -m "Add encrypted secrets" +git push +``` + +### Join Team + +```bash +# Developer 2 +git clone repo +cd repo +echo "MASTER_KEY" > .secrets.key # Received via secure channel +zeroenv ls # Verify +``` + +Encrypted `.secrets` syncs via git. Master key shared once through secure channel (password manager, encrypted messaging). + +## CI/CD + +Set `ZEROENV_MASTER_KEY` environment variable: + +### GitHub Actions +```yaml +- run: zeroenv run pytest + env: + ZEROENV_MASTER_KEY: ${{ secrets.ZEROENV_MASTER_KEY }} +``` + +### GitLab CI +```yaml +test: + script: + - zeroenv run pytest +``` + +### Azure Pipelines + +```yaml +- script: zeroenv run pytest + env: + ZEROENV_MASTER_KEY: $(ZEROENV_MASTER_KEY) +``` +## Containerization / Docker + +ZeroEnv works seamlessly with Docker. You can inject secrets into your container at runtime without baking them into the image. + +### Dockerfile + +Install `zeroenv` in your image: + +```dockerfile +FROM python:3.9-slim + +WORKDIR /app +COPY . . +RUN pip install zeroenv +RUN pip install -r requirements.txt + +# Don't set entrypoint here if you want to use zeroenv at runtime +CMD ["python", "app.py"] +``` + +### Docker Compose + +Mount the `.secrets` file and the master key (via environment variable): + +```yaml +version: '3' +services: + web: + build: . + volumes: + - ./.secrets:/app/.secrets:ro # Mount secrets file read-only + environment: + - ZEROENV_MASTER_KEY=${ZEROENV_MASTER_KEY} # Pass key from host + command: zeroenv run python app.py +``` + +### Running with Docker + +1. Export the master key on your host: + ```bash + export ZEROENV_MASTER_KEY=$(cat .secrets.key) + ``` + +2. Run the container: + ```bash + docker-compose up + ``` +``` + +## Examples + +### Basic Usage + +```bash +# Initialize with enhanced security +zeroenv init --tier enhanced + +# Check configuration +zeroenv info +# Output: +# ╭─────────────── ZeroEnv Configuration ───────────────╮ +# │ Security Tier: Enhanced │ +# │ PBKDF2 Iterations: 100,000 iterations │ +# │ Encryption: AES-256-GCM │ +# │ Secrets Stored: 0 │ +# ╰─────────────────────────────────────────────────────╯ + +# Add secrets +zeroenv add API_KEY "sk_test_1234" +zeroenv add DATABASE_URL "postgresql://localhost/db" + +# Verify +zeroenv ls --values +``` + +### Django +```bash +zeroenv init +zeroenv add SECRET_KEY "django-key" +zeroenv add DATABASE_URL "postgresql://..." +zeroenv run python manage.py runserver +``` + +### Node.js +```bash +zeroenv init +zeroenv add PORT "3000" +zeroenv add MONGODB_URI "mongodb://..." +zeroenv run npm start +``` + +### .NET +```bash +zeroenv init +zeroenv add ConnectionStrings__Default "Server=..." +zeroenv run dotnet run +``` + +## Security + +### Encryption Details + +- **Algorithm:** AES-256-GCM (AEAD) +- **Key derivation:** 256-bit random key via `os.urandom()` +- **Nonce generation:** 96-bit random nonce per encryption +- **Library:** Python `cryptography` package + +### Security Tiers + +ZeroEnv offers three security tiers to balance performance and security: + +#### Standard (Default) +- **Iterations:** 0 (direct key usage) +- **Performance:** Fastest +- **Use case:** Development, local testing +- **Security:** Strong encryption with AES-256-GCM + +```bash +zeroenv init # or explicitly: zeroenv init --tier standard +``` + +#### Enhanced +- **Iterations:** 100,000 (PBKDF2-HMAC-SHA256) +- **Performance:** Moderate (~100ms key derivation) +- **Use case:** Shared projects, small teams +- **Security:** Resistant to brute-force attacks on weak keys + +```bash +zeroenv init --tier enhanced +``` + +#### Max +- **Iterations:** 500,000 (PBKDF2-HMAC-SHA256) +- **Performance:** Slower (~500ms key derivation) +- **Use case:** Production environments, sensitive data +- **Security:** Maximum protection against brute-force attacks + +```bash +zeroenv init --tier max +``` + +**Note:** Security tier is set during initialization and stored in `.secrets`. All subsequent operations (add, get, run, export) automatically use the configured tier. Existing secrets files without a tier default to `standard` for backward compatibility. +- **Nonce generation:** 96-bit random nonce per encryption +- **Library:** Python `cryptography` package + +## Command Reference + +| Command | Description | +|---------|-------------| +| `init [--tier TIER]` | Initialize ZeroEnv in project (standard/enhanced/max) | +| `add NAME [VALUE]` | Add or update secret | +| `get NAME` | Retrieve secret value | +| `ls [--values]` | List secrets | +| `rm NAME` | Remove secret | +| `run COMMAND` | Run command with secrets | +| `export [-f FORMAT]` | Export secrets | +| `info` | Display security tier and configuration | + +## Dependencies + +- Python 3.10+ +- `click` - CLI framework +- `rich` - Terminal formatting +- `cryptography` - AES-256-GCM implementation + +## Testing + +This directory contains comprehensive unit tests to ensure reliability and security. + +## Why Testing is Critical + +I have added some unit tests to validate the core functionality of ZeroEnv, including the cryptography and storage, Data Integrity, CLI Reliability, Edge Case Handling, Regression Prevention, and Cross-Platform Compatibility. + +## Prerequisites + +The tests require `pytest` and `click`. You can install the development dependencies with: + +```bash +pip install pytest click cryptography rich +``` + +## Running Tests + +To run all tests, execute the following command from the project root: + +```bash +python -m pytest tests +``` + +To run tests with verbose output: + +```bash +python -m pytest tests -v +``` + +To run with coverage report: + +```bash +python -m pytest tests --cov=zeroenv --cov-report=html +``` + +## Test Structure + +### `test_crypto.py` - Cryptographic Operations +Tests the core security layer: +- **Key Generation**: Validates 256-bit keys are properly generated +- **Encryption**: Ensures plaintext is encrypted correctly with unique nonces +- **Decryption**: Verifies encrypted data can be decrypted back to original +- **Key Encoding**: Tests base64 encoding/decoding of keys +- **Invalid Inputs**: Tests behavior with malformed data or wrong keys +- **Nonce Uniqueness**: Ensures each encryption uses a unique nonce + +**Critical Tests:** +- Different values produce different ciphertexts +- Same value encrypted twice produces different ciphertexts (due to unique nonces) +- Decryption with wrong key fails properly +- Tampered ciphertext is detected + +### `test_storage.py` - File Operations & Data Management +Tests the persistence layer: +- **Initialization**: Creates `.secrets` and `.secrets.key` files correctly +- **File Integrity**: Validates JSON structure and metadata +- **Secret CRUD**: Add, retrieve, update, delete secrets +- **Key Loading**: Properly loads master key from file +- **Error Handling**: Missing files, corrupted data, permission errors +- **Metadata**: Timestamps and versioning work correctly + +**Critical Tests:** +- Secrets persist across program restarts +- Multiple secrets don't interfere with each other +- File permissions are appropriate +- Corrupted `.secrets` file is handled gracefully + +### `test_cli.py` - Command Line Interface +Tests the user-facing commands: +- **Init Command**: Creates files and updates .gitignore +- **Add Command**: Interactive and direct modes work +- **Get Command**: Retrieves secrets correctly +- **List Command**: Shows secrets with/without values +- **Remove Command**: Deletes secrets with confirmation +- **Run Command**: Injects environment variables properly +- **Export Command**: Outputs in .env and JSON formats +- **Error Messages**: Helpful errors for common mistakes + +**Critical Tests:** +- `zeroenv init` in already-initialized directory fails gracefully +- `zeroenv add` without init shows helpful error +- Environment variables are properly injected for `run` +- Confirmation prompts work for destructive operations +- Exit codes are correct (0 for success, 1 for errors) + +## Troubleshooting + +If you encounter issues with `test_cli.py` failing to capture output, ensure you are running the tests from the project root directory. + +**Common Issues:** +- **Import errors**: Run `pip install -e .` from project root +- **File permission errors**: Tests create temporary directories - ensure write permissions +- **Flaky tests**: Some CLI tests may be environment-dependent - use `pytest-xdist` for isolation + + +## Development + +```bash +git clone https://github.com/ropeadope62/zeroenv +cd zeroenv +pip install -e ".[dev]" +pytest +``` + +## License + +MIT + +## Credits + +Built by Dave C. (ropeadope62) +[artoffailing.com](https://artoffailing.com) + +--- + +> GitHub [@ropeadope62](https://github.com/ropeadope62) diff --git a/src/zeroenv.egg-info/SOURCES.txt b/src/zeroenv.egg-info/SOURCES.txt new file mode 100644 index 0000000..b33d09e --- /dev/null +++ b/src/zeroenv.egg-info/SOURCES.txt @@ -0,0 +1,17 @@ +LICENSE +README.md +pyproject.toml +src/zeroenv/__init__.py +src/zeroenv/cli.py +src/zeroenv/crypto.py +src/zeroenv/storage.py +src/zeroenv/ui.py +src/zeroenv.egg-info/PKG-INFO +src/zeroenv.egg-info/SOURCES.txt +src/zeroenv.egg-info/dependency_links.txt +src/zeroenv.egg-info/entry_points.txt +src/zeroenv.egg-info/requires.txt +src/zeroenv.egg-info/top_level.txt +tests/test_cli.py +tests/test_crypto.py +tests/test_storage.py \ No newline at end of file diff --git a/src/zeroenv.egg-info/dependency_links.txt b/src/zeroenv.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/zeroenv.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/zeroenv.egg-info/entry_points.txt b/src/zeroenv.egg-info/entry_points.txt new file mode 100644 index 0000000..32a98f3 --- /dev/null +++ b/src/zeroenv.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +zeroenv = zeroenv.cli:main diff --git a/src/zeroenv.egg-info/requires.txt b/src/zeroenv.egg-info/requires.txt new file mode 100644 index 0000000..02decdf --- /dev/null +++ b/src/zeroenv.egg-info/requires.txt @@ -0,0 +1,3 @@ +click +cryptography +rich diff --git a/src/zeroenv.egg-info/top_level.txt b/src/zeroenv.egg-info/top_level.txt new file mode 100644 index 0000000..98bcf33 --- /dev/null +++ b/src/zeroenv.egg-info/top_level.txt @@ -0,0 +1 @@ +zeroenv diff --git a/src/zeroenv/__pycache__/__init__.cpython-312.pyc b/src/zeroenv/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c09c6e2 Binary files /dev/null and b/src/zeroenv/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/zeroenv/__pycache__/crypto.cpython-312.pyc b/src/zeroenv/__pycache__/crypto.cpython-312.pyc new file mode 100644 index 0000000..1384a79 Binary files /dev/null and b/src/zeroenv/__pycache__/crypto.cpython-312.pyc differ diff --git a/src/zeroenv/__pycache__/storage.cpython-312.pyc b/src/zeroenv/__pycache__/storage.cpython-312.pyc new file mode 100644 index 0000000..2d31a0c Binary files /dev/null and b/src/zeroenv/__pycache__/storage.cpython-312.pyc differ diff --git a/src/zeroenv/storage.py b/src/zeroenv/storage.py index c392fb2..a7d8fca 100644 --- a/src/zeroenv/storage.py +++ b/src/zeroenv/storage.py @@ -51,7 +51,7 @@ def initialize(self, master_key: bytes, tier: str = 'standard') -> None: # Create the baseline .secrets file with metadata initial_data = { - "version": "1.0", + "version": "1.1", "created_at": datetime.now().isoformat(), "security_tier": tier, "secrets": {} @@ -129,6 +129,33 @@ def load_encryption_key(self) -> bytes: return ZeroEnvCrypto.derive_key(master_key, tier, salt) + def create_crypto(self) -> ZeroEnvCrypto: + """ + Build a ZeroEnvCrypto instance configured for the stored security tier and salt + + Returns: + ZeroEnvCrypto instance ready for encrypt/decrypt operations + """ + encryption_key = self.load_encryption_key() + return ZeroEnvCrypto(encryption_key) + + def get_project_info(self) -> dict: + """ + Get project configuration information for the info command + + Returns: + Dictionary with version, created_at, security_tier, secrets_count, + and directory path + """ + data = self.load_secrets_file() + return { + "version": data.get("version", "1.0"), + "created_at": data.get("created_at", "unknown"), + "security_tier": data.get("security_tier", "standard"), + "secrets_count": len(data.get("secrets", {})), + "directory": str(self.directory), + } + def load_secrets_file(self) -> dict: """ Load the secrets file diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..564bcdf Binary files /dev/null and b/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/tests/__pycache__/test_storage.cpython-312-pytest-9.0.3.pyc b/tests/__pycache__/test_storage.cpython-312-pytest-9.0.3.pyc new file mode 100644 index 0000000..3937c92 Binary files /dev/null and b/tests/__pycache__/test_storage.cpython-312-pytest-9.0.3.pyc differ diff --git a/tests/test_storage.py b/tests/test_storage.py index aad129b..c36adc9 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -55,7 +55,7 @@ def test_initialize(self, storage, master_key): # Verify secrets file content data = json.loads(storage.secrets_path.read_text()) - assert data['version'] == "1.0" + assert data['version'] == "1.1" assert data['secrets'] == {} def test_load_master_key(self, storage, master_key): @@ -284,3 +284,158 @@ def test_backward_compatibility_no_tier(self, temp_dir, master_key): storage.add_secret(crypto, "OLD_SECRET", "old_value") retrieved = storage.get_secret(crypto, "OLD_SECRET") assert retrieved == "old_value" + + +class TestCreateCrypto: + """Tests for create_crypto() convenience method""" + + @pytest.fixture + def temp_dir(self, tmp_path): + return str(tmp_path) + + @pytest.fixture + def master_key(self): + return generate_master_key() + + def test_create_crypto_standard_tier(self, temp_dir, master_key): + """create_crypto() returns a working ZeroEnvCrypto for standard tier""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='standard') + + crypto = storage.create_crypto() + assert isinstance(crypto, ZeroEnvCrypto) + + # Should be able to encrypt and decrypt + encrypted = crypto.encrypt("test_value") + assert crypto.decrypt(encrypted) == "test_value" + + def test_create_crypto_enhanced_tier(self, temp_dir, master_key): + """create_crypto() returns a correctly derived ZeroEnvCrypto for enhanced tier""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='enhanced') + + crypto = storage.create_crypto() + assert isinstance(crypto, ZeroEnvCrypto) + + # Verify round-trip encryption + encrypted = crypto.encrypt("secure_value") + assert crypto.decrypt(encrypted) == "secure_value" + + def test_create_crypto_max_tier(self, temp_dir, master_key): + """create_crypto() returns a correctly derived ZeroEnvCrypto for max tier""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='max') + + crypto = storage.create_crypto() + assert isinstance(crypto, ZeroEnvCrypto) + + encrypted = crypto.encrypt("max_secret") + assert crypto.decrypt(encrypted) == "max_secret" + + def test_create_crypto_consistent(self, temp_dir, master_key): + """create_crypto() is deterministic - two calls produce compatible instances""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='enhanced') + + crypto1 = storage.create_crypto() + encrypted = crypto1.encrypt("consistent_value") + + # A fresh instance should decrypt the same ciphertext + crypto2 = storage.create_crypto() + assert crypto2.decrypt(encrypted) == "consistent_value" + + def test_create_crypto_can_add_and_get_secrets(self, temp_dir, master_key): + """create_crypto() result can be used with add_secret / get_secret""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='enhanced') + + crypto = storage.create_crypto() + storage.add_secret(crypto, "KEY", "value123") + assert storage.get_secret(crypto, "KEY") == "value123" + + +class TestGetProjectInfo: + """Tests for get_project_info() method""" + + @pytest.fixture + def temp_dir(self, tmp_path): + return str(tmp_path) + + @pytest.fixture + def master_key(self): + return generate_master_key() + + def test_get_project_info_keys(self, temp_dir, master_key): + """get_project_info() returns all expected keys""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key) + + info = storage.get_project_info() + assert "version" in info + assert "created_at" in info + assert "security_tier" in info + assert "secrets_count" in info + assert "directory" in info + + def test_get_project_info_version(self, temp_dir, master_key): + """get_project_info() reports version 1.1 for newly created files""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key) + + info = storage.get_project_info() + assert info["version"] == "1.1" + + def test_get_project_info_standard_tier(self, temp_dir, master_key): + """get_project_info() reports correct tier for standard""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='standard') + + info = storage.get_project_info() + assert info["security_tier"] == "standard" + + def test_get_project_info_enhanced_tier(self, temp_dir, master_key): + """get_project_info() reports correct tier for enhanced""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='enhanced') + + info = storage.get_project_info() + assert info["security_tier"] == "enhanced" + + def test_get_project_info_secrets_count(self, temp_dir, master_key): + """get_project_info() returns correct secrets count""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key) + + crypto = storage.create_crypto() + storage.add_secret(crypto, "A", "1") + storage.add_secret(crypto, "B", "2") + + info = storage.get_project_info() + assert info["secrets_count"] == 2 + + def test_get_project_info_directory(self, temp_dir, master_key): + """get_project_info() returns the storage directory""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key) + + info = storage.get_project_info() + assert info["directory"] == temp_dir + + def test_get_project_info_backward_compatible(self, temp_dir, master_key): + """get_project_info() works with legacy v1.0 files""" + storage = SecretsStorage(temp_dir) + + # Write a v1.0 style file (no security_tier) + storage.key_path.write_text(ZeroEnvCrypto.key_to_string(master_key)) + old_data = { + "version": "1.0", + "created_at": "2023-01-01T00:00:00", + "secrets": {} + } + storage.save_secrets_file(old_data) + + info = storage.get_project_info() + assert info["version"] == "1.0" + assert info["security_tier"] == "standard" + assert info["secrets_count"] == 0 +