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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
pip install -e ".[dev]" --no-build-isolation

- name: Lint with ruff
run: pip install ruff && ruff check src/ --target-version py310
run: ruff check src/ --target-version py310

- name: Run tests
env:
Expand Down
34 changes: 34 additions & 0 deletions .secrets.baseline
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"version": "1.5.0",
"plugins_used": [
{
"name": "SecretKeywordDetector",
"version": "1.5.0",
"configuration": {
"excluded_keywords": [],
"excluded_files": [],
"keyword_exclude": []
}
},
{
"name": "HighEntropyStringDetector",
"version": "1.5.0",
"configuration": {
"entropy_limit": 3.0,
"excluded_patterns": [],
"path_patterns": []
}
}
],
"results": {
"src/envault/serve.py": [
{
"type": "Secret Keyword",
"line_number": 36,
"hashed_secret": "5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e",
"is_secret": false
}
]
},
"generated_at": "2026-06-30T00:00:00.000000Z"
}
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ Thanks for your interest in contributing!
2. Create a virtual environment: python -m venv .venv && source .venv/bin/activate
3. Install dev dependencies: pip install -e ".[dev]"
4. Run tests: pytest tests/ -v
5. Lint: uff check src/
5. Lint: ruff check src/

## Pull Requests

- Fork the repo and create a feature branch
- Add tests for any new functionality
- Ensure all existing tests pass
- Run uff check src/ --fix before committing
- Run ruff check src/ --fix before committing
- Keep PRs focused on a single change

## Reporting Issues
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ We aim to respond within 48 hours and will keep you updated on the fix.
## Security Best Practices

- Keep your dependencies up to date
- Use `pip audit` to check for known vulnerabilities
- Use `pip-audit` to check for known vulnerabilities
- Report any security concerns promptly
6 changes: 5 additions & 1 deletion _qa_repro_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""Standalone test to reproduce COM-367 behavior."""

import tempfile
from envault.cli import app
from pathlib import Path

from typer.testing import CliRunner

from envault.cli import app


def test_scan_hardcoded_credential_repro():
"""Exact reproduction of test_scan_hardcoded_credential."""
Expand Down Expand Up @@ -34,6 +37,7 @@ def test_scan_weak_password_repro():
def test_scan_json_output_repro():
"""Exact reproduction of test_scan_json_output."""
import json as _json

runner = CliRunner()
td = Path(tempfile.mkdtemp())
env_file = td / ".env"
Expand Down
7 changes: 5 additions & 2 deletions _qa_test_scan.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""QA verification script for COM-367."""

import json as _json
import os
import tempfile
from envault.cli import app

from typer.testing import CliRunner

from envault.cli import app

runner = CliRunner()

# Test 1: DB_PASSWORD with *** (defect says this should be exit_code=1)
Expand Down Expand Up @@ -32,7 +36,6 @@

# Test 3: JSON output with Rich control chars
print("=== Root Cause 3: JSON output control chars ===")
import json as _json

with tempfile.TemporaryDirectory() as td:
env_file = os.path.join(td, ".env")
Expand Down
4 changes: 1 addition & 3 deletions src/envault/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ def log(
with open(self.log_path, "a") as f:
f.write(json.dumps(entry) + "\n")

def get_history(
self, key: str | None = None, action: str | None = None, limit: int = 50
) -> list[dict]:
def get_history(self, key: str | None = None, action: str | None = None, limit: int = 50) -> list[dict]:
"""Get audit history, optionally filtered by key and/or action."""
path = Path(self.log_path)
if not path.exists():
Expand Down
27 changes: 6 additions & 21 deletions src/envault/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ def __init__(self, valid_keys: str | list[str]) -> None:
def check(self, headers: dict[str, str]) -> AuthResult:
api_key = headers.get("X-Api-Key", "") or headers.get("X-API-KEY", "")
if not api_key:
return AuthResult.fail(
401, "Unauthorized: API key required (X-API-Key header)"
)
return AuthResult.fail(401, "Unauthorized: API key required (X-API-Key header)")

if api_key not in self._keys:
return AuthResult.fail(403, "Forbidden: invalid API key")
Expand Down Expand Up @@ -126,9 +124,7 @@ def __init__(
def check(self, headers: dict[str, str]) -> AuthResult:
auth_header = headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return AuthResult.fail(
401, "Unauthorized: Bearer token required for OAuth2"
)
return AuthResult.fail(401, "Unauthorized: Bearer token required for OAuth2")

token = auth_header[len("Bearer ") :]

Expand Down Expand Up @@ -173,9 +169,7 @@ def _introspect(self, token: str) -> AuthResult:
"Content-Type": "application/x-www-form-urlencoded",
}
if self._client_id and self._client_secret:
creds = base64.b64encode(
f"{self._client_id}:{self._client_secret}".encode()
).decode()
creds = base64.b64encode(f"{self._client_id}:{self._client_secret}".encode()).decode()
headers["Authorization"] = f"Basic {creds}"

req = Request(url, data=body, headers=headers, method="POST")
Expand All @@ -195,9 +189,7 @@ def _validate_claims(self, token: str, claims: dict[str, Any]) -> AuthResult:
required = set(self._required_scope.split())
if not required.issubset(token_scopes):
missing = required - token_scopes
return AuthResult.fail(
403, f"Forbidden: missing scope(s): {', '.join(sorted(missing))}"
)
return AuthResult.fail(403, f"Forbidden: missing scope(s): {', '.join(sorted(missing))}")

# Audience check
if self._required_audience:
Expand All @@ -207,12 +199,7 @@ def _validate_claims(self, token: str, claims: dict[str, Any]) -> AuthResult:
if self._required_audience not in audiences:
return AuthResult.fail(403, "Forbidden: invalid audience")

identity = (
claims.get("sub")
or claims.get("email")
or claims.get("client_id")
or "oauth2:user"
)
identity = claims.get("sub") or claims.get("email") or claims.get("client_id") or "oauth2:user"

# Cache the successful result
self._cache[token] = (identity, time.monotonic() + self._cache_ttl)
Expand All @@ -226,9 +213,7 @@ class MultiAuth:
If no backend is configured, all requests are allowed (open mode).
"""

def __init__(
self, backends: list[BearerAuth | ApiKeyAuth | OAuth2Auth] | None = None
) -> None:
def __init__(self, backends: list[BearerAuth | ApiKeyAuth | OAuth2Auth] | None = None) -> None:
self._backends = backends or []

@property
Expand Down
6 changes: 1 addition & 5 deletions src/envault/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,5 @@ def format_backup_list(entries: list[BackupEntry]) -> str:
lines = []
for entry in entries:
enc_tag = " [encrypted]" if entry.encrypted else ""
lines.append(
f" {entry.name}{enc_tag}\n"
f" Source: {entry.source_file}\n"
f" Created: {entry.timestamp}"
)
lines.append(f" {entry.name}{enc_tag}\n Source: {entry.source_file}\n Created: {entry.timestamp}")
return "\n".join(lines)
Loading
Loading