Files
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

273 lines
11 KiB
Python

#!/usr/bin/env python3
"""HashiCorp Vault secrets management agent.
Manages secrets lifecycle using the HashiCorp Vault KV v2 secrets engine
via the hvac Python library. Supports reading, writing, listing, deleting
secrets, and auditing secret access patterns.
"""
import argparse
import json
import os
import sys
from datetime import datetime, timezone
try:
import hvac
except ImportError:
print("[!] 'hvac' library required: pip install hvac", file=sys.stderr)
sys.exit(1)
def get_vault_client(addr=None, token=None, namespace=None):
"""Create and authenticate a Vault client."""
vault_addr = addr or os.environ.get("VAULT_ADDR", "http://127.0.0.1:8200")
vault_token = token or os.environ.get("VAULT_TOKEN", "")
vault_ns = namespace or os.environ.get("VAULT_NAMESPACE")
if not vault_token:
print("[!] Set VAULT_TOKEN env var or use --token", file=sys.stderr)
sys.exit(1)
client = hvac.Client(url=vault_addr, token=vault_token, namespace=vault_ns)
if not client.is_authenticated():
print("[!] Vault authentication failed", file=sys.stderr)
sys.exit(1)
print(f"[+] Connected to Vault at {vault_addr}")
return client
def write_secret(client, path, data, mount_point="secret"):
"""Write or update a secret in KV v2."""
print(f"[*] Writing secret to {mount_point}/{path}")
response = client.secrets.kv.v2.create_or_update_secret(
path=path, secret=data, mount_point=mount_point,
)
version = response.get("data", {}).get("version", "unknown")
print(f"[+] Secret written (version: {version})")
return {"path": path, "version": version, "action": "write"}
def read_secret(client, path, mount_point="secret", version=None):
"""Read a secret from KV v2."""
print(f"[*] Reading secret from {mount_point}/{path}")
kwargs = {"path": path, "mount_point": mount_point}
if version:
kwargs["version"] = version
response = client.secrets.kv.v2.read_secret_version(**kwargs)
secret_data = response.get("data", {}).get("data", {})
metadata = response.get("data", {}).get("metadata", {})
print(f"[+] Secret read (version: {metadata.get('version', '?')}, "
f"created: {metadata.get('created_time', 'unknown')})")
masked = {k: v[:3] + "***" if isinstance(v, str) and len(v) > 3 else "***"
for k, v in secret_data.items()}
return {
"path": path,
"keys": list(secret_data.keys()),
"masked_values": masked,
"version": metadata.get("version"),
"created_time": metadata.get("created_time"),
"deletion_time": metadata.get("deletion_time"),
"destroyed": metadata.get("destroyed"),
}
def list_secrets(client, path="", mount_point="secret"):
"""List secret paths under a given prefix."""
print(f"[*] Listing secrets under {mount_point}/{path}")
try:
response = client.secrets.kv.v2.list_secrets(
path=path, mount_point=mount_point
)
keys = response.get("data", {}).get("keys", [])
print(f"[+] Found {len(keys)} entries")
for key in keys:
marker = "/" if key.endswith("/") else " "
print(f" {marker} {key}")
return {"path": path, "entries": keys, "count": len(keys)}
except hvac.exceptions.InvalidPath:
print(f"[*] No secrets found at {mount_point}/{path}")
return {"path": path, "entries": [], "count": 0}
def delete_secret(client, path, mount_point="secret", versions=None, destroy=False):
"""Delete or destroy a secret."""
if destroy and versions:
print(f"[*] Permanently destroying versions {versions} at {mount_point}/{path}")
client.secrets.kv.v2.destroy_secret_versions(
path=path, versions=versions, mount_point=mount_point
)
print(f"[+] Versions {versions} permanently destroyed")
return {"path": path, "action": "destroy", "versions": versions}
elif versions:
print(f"[*] Soft-deleting versions {versions} at {mount_point}/{path}")
client.secrets.kv.v2.delete_secret_versions(
path=path, versions=versions, mount_point=mount_point
)
print(f"[+] Versions {versions} soft-deleted (recoverable)")
return {"path": path, "action": "soft_delete", "versions": versions}
else:
print(f"[*] Deleting latest version at {mount_point}/{path}")
client.secrets.kv.v2.delete_latest_version_of_secret(
path=path, mount_point=mount_point
)
print(f"[+] Latest version deleted")
return {"path": path, "action": "delete_latest"}
def read_metadata(client, path, mount_point="secret"):
"""Read secret metadata including version history."""
print(f"[*] Reading metadata for {mount_point}/{path}")
response = client.secrets.kv.v2.read_secret_metadata(
path=path, mount_point=mount_point
)
meta = response.get("data", {})
versions = meta.get("versions", {})
print(f"[+] {len(versions)} version(s), max_versions: {meta.get('max_versions', 0)}")
version_info = []
for ver_num, ver_data in sorted(versions.items(), key=lambda x: int(x[0])):
version_info.append({
"version": int(ver_num),
"created_time": ver_data.get("created_time"),
"deletion_time": ver_data.get("deletion_time"),
"destroyed": ver_data.get("destroyed", False),
})
return {
"path": path,
"current_version": meta.get("current_version"),
"max_versions": meta.get("max_versions"),
"cas_required": meta.get("cas_required"),
"created_time": meta.get("created_time"),
"updated_time": meta.get("updated_time"),
"versions": version_info,
}
def audit_secrets(client, mount_point="secret", path_prefix=""):
"""Audit all secrets under a path, reporting metadata and age."""
print(f"[*] Auditing secrets under {mount_point}/{path_prefix}")
audit_results = []
def _recurse(current_path):
try:
resp = client.secrets.kv.v2.list_secrets(
path=current_path, mount_point=mount_point
)
except hvac.exceptions.InvalidPath:
return
keys = resp.get("data", {}).get("keys", [])
for key in keys:
full_path = f"{current_path}{key}" if current_path else key
if key.endswith("/"):
_recurse(full_path)
else:
try:
meta_resp = client.secrets.kv.v2.read_secret_metadata(
path=full_path, mount_point=mount_point
)
meta = meta_resp.get("data", {})
audit_results.append({
"path": full_path,
"current_version": meta.get("current_version"),
"created_time": meta.get("created_time"),
"updated_time": meta.get("updated_time"),
"version_count": len(meta.get("versions", {})),
})
except Exception as e:
audit_results.append({"path": full_path, "error": str(e)})
_recurse(path_prefix)
print(f"[+] Audited {len(audit_results)} secret(s)")
for item in audit_results:
if "error" in item:
print(f" [ERR] {item['path']}: {item['error']}")
else:
print(f" {item['path']:40s} v{item['current_version']} "
f"(updated: {str(item.get('updated_time', 'N/A'))[:19]})")
return audit_results
def main():
parser = argparse.ArgumentParser(
description="HashiCorp Vault secrets management agent"
)
sub = parser.add_subparsers(dest="command", help="Action to perform")
p_write = sub.add_parser("write", help="Write a secret")
p_write.add_argument("--path", required=True, help="Secret path")
p_write.add_argument("--data", required=True, help='JSON data')
p_write.add_argument("--mount", default="secret", help="KV mount point")
p_read = sub.add_parser("read", help="Read a secret")
p_read.add_argument("--path", required=True)
p_read.add_argument("--version", type=int, help="Specific version")
p_read.add_argument("--mount", default="secret")
p_list = sub.add_parser("list", help="List secrets")
p_list.add_argument("--path", default="")
p_list.add_argument("--mount", default="secret")
p_del = sub.add_parser("delete", help="Delete a secret")
p_del.add_argument("--path", required=True)
p_del.add_argument("--versions", type=int, nargs="+")
p_del.add_argument("--destroy", action="store_true")
p_del.add_argument("--mount", default="secret")
p_meta = sub.add_parser("metadata", help="Read secret metadata")
p_meta.add_argument("--path", required=True)
p_meta.add_argument("--mount", default="secret")
p_audit = sub.add_parser("audit", help="Audit all secrets")
p_audit.add_argument("--path", default="")
p_audit.add_argument("--mount", default="secret")
parser.add_argument("--addr", help="Vault address (or VAULT_ADDR env)")
parser.add_argument("--token", help="Vault token (or VAULT_TOKEN env)")
parser.add_argument("--namespace", help="Vault namespace")
parser.add_argument("--output", "-o", help="Output JSON report")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
client = get_vault_client(args.addr, args.token, args.namespace)
if args.command == "write":
secret_data = json.loads(args.data)
result = write_secret(client, args.path, secret_data, args.mount)
elif args.command == "read":
result = read_secret(client, args.path, args.mount,
getattr(args, "version", None))
elif args.command == "list":
result = list_secrets(client, args.path, args.mount)
elif args.command == "delete":
result = delete_secret(client, args.path, args.mount,
getattr(args, "versions", None),
getattr(args, "destroy", False))
elif args.command == "metadata":
result = read_metadata(client, args.path, args.mount)
elif args.command == "audit":
result = audit_secrets(client, args.mount, args.path)
else:
parser.print_help()
sys.exit(1)
report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"tool": "HashiCorp Vault",
"command": args.command,
"result": result,
}
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"\n[+] Report saved to {args.output}")
elif args.verbose:
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()