#!/usr/bin/env python3 import argparse import getpass import shutil import subprocess as sp from pathlib import Path from typing import Literal import jinja2 bitwarden_session = None TemplateEnvType = Literal["password", "username"] def fetch_secret(bw_path: Path, secret_id: str, object_type: str = "password") -> str: global bitwarden_session res = sp.run( [bw_path, "get", object_type, secret_id, "--session", bitwarden_session], capture_output=True, text=True, ) res.check_returncode() return res.stdout class TemplateEnv: bw_path: Path env_type: TemplateEnvType cached_items: dict[str, str] def __init__(self, _type: TemplateEnvType, bw_path: Path): self.env_type = _type self.bw_path = bw_path self.cached_items = dict() def __getitem__(self, item): print(f"{self.env_type} {self.cached_items}") if cached_item := self.cached_items.get(item): return cached_item self.cached_items[item] = fetch_secret( self.bw_path, item, self.env_type, ) return self.cached_items[item] def _add_args(parser: argparse.ArgumentParser): parser.add_argument( "search_paths", help="start directory to walk files to find secret references", default=".", nargs="*", ) parser.add_argument( "--bw-path", "-b", help="custom path for bitwarden cli executable", default=shutil.which("bw"), ) def init_bw_session(bw_path: Path): global bitwarden_session if (pw_file := Path("./.bw2secrets")).exists(): bitwarden_password = pw_file.read_text().strip() else: print("Please, provide your bitwarden master password") bitwarden_password = getpass.getpass("Master password: ") res = sp.run( [bw_path, "unlock", bitwarden_password, "--raw"], capture_output=True, text=True, ) res.check_returncode() bitwarden_session = res.stdout def sync_bw_session(bw_path: Path): global bitwarden_session res = sp.run( [bw_path, "sync", "--session", bitwarden_session], capture_output=True, text=True, ) res.check_returncode() def find_templates(base_dirs: set[Path]) -> set[Path]: env_templates: set[Path] = set() for path in base_dirs: for env_template in path.glob("**/*.template"): env_templates.add(env_template) return env_templates # def secret_filter(bw_path: Path, secret_id: str) -> str: # return fetch_secret(bw_path, secret_id) def compile_file(file_path: Path, bw_path: Path): jinja_env = jinja2.Environment( loader=jinja2.FileSystemLoader(file_path.parent), ) username = TemplateEnv("username", bw_path) password = TemplateEnv("password", bw_path) template = jinja_env.get_template(file_path.name) rendered_template = template.render( dict( username=username, password=password, ), ) file_path.with_name( file_path.name.replace( ".template", "", ), ).write_text(rendered_template) def main() -> int: parser = argparse.ArgumentParser("bw2secrets") _add_args(parser) args = parser.parse_args() if not (bw_path := args.bw_path): print("Bitwarden CLI `bw` executable not found in PATH") return 1 search_paths: set[Path | None] = set() for path in args.search_paths: search_path = Path(path) search_paths.add(search_path) template_files = find_templates(search_paths) init_bw_session(bw_path) sync_bw_session(bw_path) for file in template_files: compile_file(file, bw_path) return 0 if __name__ == "__main__": raise SystemExit(main())