fix(api,skills): harden secrets door — no secret value to stdout (DPLAN-0211, clears CodeQL #86-88)
PR #640's only failing required check was Code scanning/CodeQL: 3 HIGH py/clear-text-logging-sensitive-data alerts where get-secret printed raw secret values to stdout. Research (OWASP, CodeQL rule source, secret-CLI survey) confirmed a real exposure — acute for AIPass since it runs inside Claude Code, which captures command stdout into model context, and the telegram skill shelled out to get-secret and parsed the token from stdout. @api (P1): - NEW apps/modules/secrets.py — in-process cross-branch door (get_secret, list_secrets) wrapping the auth handler; consumers import this, not the CLI. - get_secret_cmd rewritten: masked summary by default ('slug: set (N chars)'), --out FILE writes the raw value 0o600 and prints only the path, --list shows slug names via console.print. All 3 raw-value print() sinks removed. - bypass.json reasoning + README + help updated. @skills (P2): - telegram config._get_secret / list_bot_configs rewired from subprocess+stdout parse to the in-process aipass.api.apps.modules.secrets API; subprocess/json imports dropped. Tests + SKILL.md updated. Also: seedgo test_checkers_batch2.py — comment the synthetic sk-or-v1 fixture keys as FAKE (not real credentials). Verified: @api 504 tests + seedgo 100%; telegram 452/452; skills 252/252; no secret reaches stdout by any path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c8ae084f54
commit
a75910a4e6
@@ -0,0 +1,127 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: secrets.py
|
||||
# Description: Secrets Module — cross-branch in-process door
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-06-15
|
||||
# Modified: 2026-06-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Secrets Module
|
||||
|
||||
Cross-branch in-process API for reading secrets from the provider store.
|
||||
Consumers import directly instead of shelling out to the CLI.
|
||||
|
||||
Functions:
|
||||
get_secret() - Read a secret by provider/slug
|
||||
list_secrets() - List available slugs for a provider
|
||||
handle_command() - Route CLI commands (seedgo module discovery)
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from aipass.prax import logger # noqa: F401 — seedgo imports standard
|
||||
from aipass.cli.apps.modules import console, header
|
||||
from aipass.api.apps.handlers.json import json_handler
|
||||
from aipass.api.apps.handlers.auth import secrets as _handler
|
||||
|
||||
|
||||
def print_introspection():
|
||||
"""Show module introspection - connected handlers and capabilities"""
|
||||
console.print()
|
||||
header("Secrets Module Introspection")
|
||||
console.print()
|
||||
|
||||
console.print("[cyan]Purpose:[/cyan] Cross-branch secrets access (in-process)")
|
||||
console.print()
|
||||
|
||||
console.print("[cyan]Connected Handlers:[/cyan]")
|
||||
console.print(" • api.apps.handlers.auth.secrets")
|
||||
console.print()
|
||||
|
||||
console.print("[cyan]Available Workflows:[/cyan]")
|
||||
console.print(" • get_secret() - Read secret by provider/slug")
|
||||
console.print(" • list_secrets() - List slugs for a provider")
|
||||
console.print()
|
||||
|
||||
|
||||
def print_help():
|
||||
"""Print help output for secrets module"""
|
||||
print_introspection()
|
||||
|
||||
|
||||
def handle_command(command: str, args: List[str]) -> bool:
|
||||
"""
|
||||
Handle secrets commands (module discovery hook).
|
||||
|
||||
This module does not own any CLI commands — get-secret is routed
|
||||
through api_key.py. This exists for seedgo module discovery only.
|
||||
|
||||
Args:
|
||||
command: Command name
|
||||
args: Command arguments
|
||||
|
||||
Returns:
|
||||
False — no commands handled here
|
||||
"""
|
||||
if not args:
|
||||
print_introspection()
|
||||
return True
|
||||
|
||||
if args[0] in ("--help", "-h", "help"):
|
||||
print_help()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_secret(provider: str, slug: str, as_json: bool = False) -> Optional[Any]:
|
||||
"""
|
||||
Read a secret from the provider store.
|
||||
|
||||
This is the sanctioned cross-branch import path. Consumers call this
|
||||
instead of shelling out to 'drone @api get-secret'.
|
||||
|
||||
Args:
|
||||
provider: Provider directory name (e.g., 'telegram', 'openrouter')
|
||||
slug: Secret identifier (without .json extension)
|
||||
as_json: If True, return full parsed dict; otherwise extract primary token
|
||||
|
||||
Returns:
|
||||
Secret value (str or dict) or None if not found
|
||||
"""
|
||||
result = _handler.get_secret(provider, slug, as_json=as_json)
|
||||
json_handler.log_operation("secrets_get", {"provider": provider, "slug": slug, "found": result is not None})
|
||||
return result
|
||||
|
||||
|
||||
def list_secrets(provider: str) -> List[str]:
|
||||
"""
|
||||
List available secret slugs for a provider.
|
||||
|
||||
Args:
|
||||
provider: Provider directory name
|
||||
|
||||
Returns:
|
||||
Sorted list of slug names
|
||||
"""
|
||||
return _handler.list_secrets(provider)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""Standalone execution mode"""
|
||||
args = sys.argv[1:]
|
||||
|
||||
if len(args) == 0:
|
||||
print_introspection()
|
||||
sys.exit(0)
|
||||
|
||||
if args[0] in ["--help", "-h", "help"]:
|
||||
print_help()
|
||||
sys.exit(0)
|
||||
|
||||
console.print()
|
||||
console.print(f"[red]Unknown command: {args[0]}[/red]")
|
||||
console.print()
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user