from __future__ import annotations from collections import Counter, defaultdict from typing import List, Dict, Any, Tuple, Optional from .deps_pipeline import dedupe_effective def _sec(sections: Dict[str, Any] | None, key: str, default: bool) -> bool: """Read a boolean 'sections' flag with a default.""" if not isinstance(sections, dict): return default v = sections.get(key, default) return bool(v) def _sec_str(sections: Dict[str, Any] | None, key: str, default: str) -> str: if not isinstance(sections, dict): return default v = sections.get(key, default) return str(v) def _is_dev_scope(eco: str, scope: str | None) -> bool: scope_l = (scope or "").lower() if eco == "npm": return scope_l == "devdependencies" if eco == "pypi": return ("dev" in scope_l) or ("test" in scope_l) or scope_l.startswith("optional:") if eco in {"maven", "gradle"}: return ("test" in scope_l) or scope_l in {"provided"} return False def split_important_and_dev(deps_eff: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: important, dev = [], [] for d in deps_eff: eco = d.get("ecosystem") or "unknown" if _is_dev_scope(str(eco), d.get("scope")): dev.append(d) else: important.append(d) return important, dev def print_deps_grouped(title: str, deps_list: List[Dict[str, Any]], max_per_eco: int) -> None: print(f" {title}:") if not deps_list: print(" - (нет)") return by_eco: dict[str, List[Dict[str, Any]]] = defaultdict(list) for d in deps_list: by_eco[str(d.get("ecosystem") or "unknown")].append(d) for eco in sorted(by_eco.keys()): items = sorted(by_eco[eco], key=lambda x: (str(x.get("name") or ""))) total = len(items) print(f" [{eco}] {total}") for i, d in enumerate(items): if i >= max_per_eco: print(f" ... ещё {total - i}") break name = d.get("name") spec = d.get("spec") scope = d.get("scope") or d.get("note") line = f" - {name}" if spec: line += f" [{spec}]" if scope: line += f" ({scope})" print(line) def print_container_report( container: Dict[str, Any], *, sections: Optional[Dict[str, Any]] = None, max_deps_per_ecosystem: int = 20, max_dev_deps_per_ecosystem: int = 10, top_affected: int = 8, ) -> None: if _sec(sections, "show_separator", False): print("=" * 88) else: print() print(f"Контейнер: {container.get('name')} Образ: {container.get('image')}") if _sec(sections, "show_id_status", True): line = f" ID: {container.get('id')} Статус: {container.get('status')}" if _sec(sections, "show_created", False): line += f" Создан: {container.get('create_time')}" print(line) if _sec(sections, "show_ports", False): print(f" Порты: {container.get('ports')}") if _sec(sections, "show_mounts", False): mounts = container.get("mounted_data") or [] if isinstance(mounts, list) and mounts: print(f" Маунты: {len(mounts)}") for m in mounts[:5]: if isinstance(m, dict): src = m.get("Source") or m.get("Name") or "?" dst = m.get("Destination") or "?" mode = m.get("Mode") or "" rw = "rw" if m.get("RW") else "ro" extra = f" ({mode},{rw})" if mode else f" ({rw})" print(f" - {src} -> {dst}{extra}") if len(mounts) > 5: print(f" ... ещё {len(mounts) - 5}") else: print(" Маунты: 0") if _sec(sections, "show_versions", True): print(f" Версия: tag={container.get('version')} label={container.get('image_version_label')}") if _sec(sections, "show_language", True): print(f" Язык: {container.get('language')}") if _sec(sections, "show_source", True): print(f" Source repo: {container.get('source_url')}") if _sec(sections, "show_revision", True): print(f" Revision: {container.get('source_revision')}") if _sec(sections, "show_deps_source", True): print(f" Источник зависимостей: {container.get('deps_source')}") if _sec(sections, "show_manifests", True): manifests = container.get("dep_manifests_used") or [] mode = _sec_str(sections, "manifests_mode", "count").lower() if not isinstance(manifests, list): manifests = [] if mode == "list": print(f" Манифесты ({len(manifests)}): " + (", ".join(str(x) for x in manifests) if manifests else "(нет)")) else: print(f" Манифесты: {len(manifests)}") raw_deps = container.get("dependencies") or [] if _sec(sections, "show_raw_deps_count", False): print(f" Зависимостей (сырых): {len(raw_deps)}") deps_eff = dedupe_effective(raw_deps) if _sec(sections, "show_effective_deps_count", True): print(f" Зависимости: {len(deps_eff)} (после дедупликации)") if _sec(sections, "show_ecosystem_counts", True): eco_counts = Counter(d.get("ecosystem") for d in deps_eff if d.get("ecosystem")) if eco_counts: print(" По экосистемам: " + ", ".join(f"{k}={v}" for k, v in sorted(eco_counts.items()))) if _sec(sections, "show_deps_list", False): important, dev = split_important_and_dev(deps_eff) print_deps_grouped("ВАЖНЫЕ (для CVE/анализа)", important, max_per_eco=max_deps_per_ecosystem) if dev and _sec(sections, "show_dev_deps_list", False): print_deps_grouped("DEV/TEST (при наличии)", dev, max_per_eco=max_dev_deps_per_ecosystem) # OSV summary (after severity filtering) if _sec(sections, "show_osv_summary", True): if container.get("osv_errors"): print(f" OSV: ОШИБКА: {container.get('osv_errors')}") else: pinned = container.get("osv_pinned_deps") or [] vuln_count = int(container.get("osv_vuln_count") or 0) affected = container.get("osv_affected_deps") or [] counts = container.get("osv_vuln_counts_by_severity") or {} sev_summary = "" if isinstance(counts, dict) and counts: ordered = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"] parts = [f"{k}={int(counts.get(k, 0))}" for k in ordered if k in counts or k == "UNKNOWN"] sev_summary = " (" + ", ".join(parts) + ")" if parts else "" print(f" OSV: уязвимости={vuln_count}; затронутые пакеты={len(affected)}; проверено пакетов={len(pinned)}{sev_summary}") if affected and _sec(sections, "show_osv_top_affected", True): print(" OSV: топ уязвимых зависимостей:") for row in affected[:top_affected]: if len(row) >= 5: eco, name, ver, cnt, max_sev = row[:5] print(f" - {eco}:{name}@{ver} -> {cnt} ids (max={max_sev})") else: eco, name, ver, cnt = row[:4] print(f" - {eco}:{name}@{ver} -> {cnt} ids") if _sec(sections, "show_osv_sample_ids", False): top_key = f"{affected[0][0]}:{affected[0][1]}@{affected[0][2]}" ids = (container.get("osv_vulns_by_dep") or {}).get(top_key, []) if ids: print(" OSV: примеры vuln_ids (первые 10): " + ", ".join(ids[:10])) if _sec(sections, "show_errors", True): errs = container.get("dep_errors") or [] if errs: print(" Примечания/ошибки (первые 8):") for e in errs[:8]: print(f" ! {e}") if len(errs) > 8: print(f" ... ещё {len(errs) - 8}") if _sec(sections, "show_code_files", False): print(f" code_files: {container.get('code_files')}") def _service_key(container: Dict[str, Any]) -> str: labels = container.get("all_labels") or container.get("labels") or {} if isinstance(labels, dict): svc = labels.get("com.docker.compose.service") proj = labels.get("com.docker.compose.project") if svc: return f"{proj}:{svc}" if proj else str(svc) name = str(container.get("name") or "") m = name.rsplit("_", 1) if len(m) == 2 and m[1].isdigit(): return m[0] image = str(container.get("image") or "") base = image.split("/")[-1] base = base.split(":", 1)[0] return base or name or "unknown" def print_report( containers: List[Dict[str, Any]], *, group_by_service: bool = True, sections: Optional[Dict[str, Any]] = None, max_deps_per_ecosystem: int = 20, max_dev_deps_per_ecosystem: int = 10, top_affected: int = 8, ) -> None: if not group_by_service: for c in containers: print_container_report( c, sections=sections, max_deps_per_ecosystem=max_deps_per_ecosystem, max_dev_deps_per_ecosystem=max_dev_deps_per_ecosystem, top_affected=top_affected, ) return groups: Dict[str, List[Dict[str, Any]]] = defaultdict(list) for c in containers: groups[_service_key(c)].append(c) for key in sorted(groups.keys()): if _sec(sections, "show_service_separator", True): print("─" * 88) if _sec(sections, "show_service_header", True): print(f"Сервис: {key} Контейнеров: {len(groups[key])}") for c in groups[key]: print_container_report( c, sections=sections, max_deps_per_ecosystem=max_deps_per_ecosystem, max_dev_deps_per_ecosystem=max_dev_deps_per_ecosystem, top_affected=top_affected, )