#!/usr/bin/env python3 """Agent for securing container registry with Harbor. Audits Harbor registry security configuration including RBAC, vulnerability scanning policies, content trust, immutable tags, and OIDC authentication via Harbor REST API v2.0. """ import json import sys from pathlib import Path from datetime import datetime try: import requests except ImportError: requests = None class HarborSecurityAgent: """Audits and hardens Harbor container registry security.""" def __init__(self, harbor_url, username="admin", password="", output_dir="./harbor_audit"): self.base_url = harbor_url.rstrip("/") + "/api/v2.0" self.auth = (username, password) self.output_dir = Path(output_dir) self.output_dir.mkdir(parents=True, exist_ok=True) self.findings = [] def _get(self, path, params=None): if not requests: return {"error": "requests library required"} resp = requests.get(f"{self.base_url}{path}", auth=self.auth, params=params, verify=True, timeout=15) try: return resp.json() except (json.JSONDecodeError, ValueError): return {"status": resp.status_code} def audit_projects(self): """Audit all projects for security configuration.""" projects = self._get("/projects", {"page_size": 100}) if isinstance(projects, dict) and "error" in projects: return projects results = [] for p in projects: meta = p.get("metadata", {}) name = p.get("name", "") issues = [] if meta.get("public") == "true": issues.append("Project is public - images accessible without auth") if meta.get("auto_scan") != "true": issues.append("Auto-scan on push not enabled") if meta.get("prevent_vul") != "true": issues.append("Vulnerability prevention not enabled") if meta.get("enable_content_trust") != "true": issues.append("Content trust (Notary) not enabled") if meta.get("enable_content_trust_cosign") != "true": issues.append("Cosign content trust not enabled") for issue in issues: self.findings.append({ "severity": "high" if "public" in issue or "prevent_vul" in issue else "medium", "project": name, "issue": issue, }) results.append({ "name": name, "public": meta.get("public"), "auto_scan": meta.get("auto_scan"), "prevent_vul": meta.get("prevent_vul"), "content_trust": meta.get("enable_content_trust"), "cosign": meta.get("enable_content_trust_cosign"), "issues": issues, }) return results def audit_system_config(self): """Check system-level security configuration.""" config = self._get("/configurations") if isinstance(config, dict) and "error" in config: return config checks = [] auth_mode = config.get("auth_mode", {}).get("value", "db_auth") if auth_mode == "db_auth": self.findings.append({ "severity": "medium", "issue": "Using local database auth instead of OIDC/LDAP", }) checks.append({"check": "auth_mode", "value": auth_mode, "status": "WARN"}) else: checks.append({"check": "auth_mode", "value": auth_mode, "status": "OK"}) self_reg = config.get("self_registration", {}).get("value", True) if self_reg: self.findings.append({ "severity": "high", "issue": "Self-registration enabled - anyone can create accounts", }) checks.append({"check": "self_registration", "value": self_reg, "status": "FAIL" if self_reg else "OK"}) return checks def list_project_members(self, project_name): """List members and their roles for a project.""" members = self._get(f"/projects/{project_name}/members") if isinstance(members, dict) and "error" in members: return members role_map = {1: "ProjectAdmin", 2: "Maintainer", 3: "Developer", 4: "Guest", 5: "LimitedGuest"} return [ {"username": m.get("entity_name", ""), "role": role_map.get(m.get("role_id"), "Unknown")} for m in (members if isinstance(members, list) else []) ] def check_immutable_tags(self, project_name): """Check immutable tag rules for a project.""" rules = self._get(f"/projects/{project_name}/immutabletagrules") if not rules or (isinstance(rules, dict) and "error" in rules): self.findings.append({ "severity": "medium", "project": project_name, "issue": "No immutable tag rules configured", }) return [] return rules def audit_logs(self, page_size=20): """Retrieve recent audit log entries.""" logs = self._get("/audit-logs", {"page_size": page_size}) if isinstance(logs, dict) and "error" in logs: return logs return [ {"operation": l.get("operation"), "resource": l.get("resource"), "username": l.get("username"), "time": l.get("op_time")} for l in (logs if isinstance(logs, list) else []) ] def generate_report(self): projects = self.audit_projects() sys_config = self.audit_system_config() report = { "report_date": datetime.utcnow().isoformat(), "harbor_url": self.base_url.replace("/api/v2.0", ""), "projects_audited": len(projects) if isinstance(projects, list) else 0, "system_config": sys_config, "projects": projects, "findings": self.findings, "finding_count": len(self.findings), } out = self.output_dir / "harbor_audit_report.json" with open(out, "w") as f: json.dump(report, f, indent=2) print(json.dumps(report, indent=2)) return report def main(): if len(sys.argv) < 2: print("Usage: agent.py [--user admin] [--pass Harbor12345]") sys.exit(1) url = sys.argv[1] user = "admin" password = "Harbor12345" if "--user" in sys.argv: user = sys.argv[sys.argv.index("--user") + 1] if "--pass" in sys.argv: password = sys.argv[sys.argv.index("--pass") + 1] agent = HarborSecurityAgent(url, user, password) agent.generate_report() if __name__ == "__main__": main()