163 lines
5.1 KiB
Python
163 lines
5.1 KiB
Python
import docker
|
|
import re
|
|
from typing import List, Tuple, Optional, Dict
|
|
|
|
SAFE_VERSIONS: Dict[str, str] = { #Потом тут будет взятие версий и CVE из базы CVE
|
|
"nginx": "1.25.4",
|
|
"redis": "7.2.4",
|
|
"postgres": "16.2",
|
|
"mysql": "8.0.36",
|
|
}
|
|
|
|
|
|
def parse_image_ref(image_ref: str) -> Tuple[str, str]:
|
|
if ":" in image_ref:
|
|
repo, tag = image_ref.rsplit(":", 1)
|
|
else:
|
|
repo, tag = image_ref, "latest"
|
|
name = repo.split("/")[-1]
|
|
return name, tag
|
|
|
|
|
|
def extract_version_from_tag(tag: str) -> Optional[str]:
|
|
tag = tag.strip()
|
|
if tag in ("latest", "", "<none>"):
|
|
return None
|
|
base = re.split(r"[^0-9.]", tag)[0]
|
|
if not base or not re.search(r"\d", base):
|
|
return None
|
|
return base
|
|
|
|
|
|
def version_tuple(v: str) -> Tuple[int, ...]:
|
|
parts = v.split(".")
|
|
nums: List[int] = []
|
|
for p in parts:
|
|
if p.isdigit():
|
|
nums.append(int(p))
|
|
else:
|
|
# На всякий случай вытащим ведущие цифры
|
|
m = re.match(r"(\d+)", p)
|
|
if m:
|
|
nums.append(int(m.group(1)))
|
|
return tuple(nums)
|
|
|
|
|
|
def compare_versions(v1: str, v2: str) -> int:
|
|
|
|
t1 = version_tuple(v1)
|
|
t2 = version_tuple(v2)
|
|
max_len = max(len(t1), len(t2))
|
|
t1 += (0,) * (max_len - len(t1))
|
|
t2 += (0,) * (max_len - len(t2))
|
|
if t1 < t2:
|
|
return -1
|
|
if t1 > t2:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def detect_service(name: str) -> Optional[str]:
|
|
lname = name.lower()
|
|
for service in SAFE_VERSIONS.keys():
|
|
if service in lname:
|
|
return service
|
|
return None
|
|
|
|
|
|
def main() -> None:
|
|
client = docker.from_env()
|
|
containers = client.containers.list(all=True)
|
|
|
|
rows = []
|
|
for c in containers:
|
|
image_tags = c.image.tags
|
|
if image_tags:
|
|
image_ref = image_tags[0]
|
|
else:
|
|
image_ref = c.attrs["Config"]["Image"]
|
|
|
|
service_name_raw, tag = parse_image_ref(image_ref)
|
|
service = detect_service(service_name_raw)
|
|
version = extract_version_from_tag(tag)
|
|
|
|
status = "UNKNOWN"
|
|
note = ""
|
|
|
|
if service is None:
|
|
status = "SKIP"
|
|
note = "Неизвестный сервис (не в списке SAFE_VERSIONS)"
|
|
elif version is None:
|
|
status = "UNKNOWN"
|
|
note = "Не удалось определить версию из тега"
|
|
else:
|
|
safe_ver = SAFE_VERSIONS.get(service)
|
|
if safe_ver is None:
|
|
status = "UNKNOWN"
|
|
note = "Нет эталонной версии для сравнения"
|
|
else:
|
|
cmp_res = compare_versions(version, safe_ver)
|
|
if cmp_res < 0:
|
|
status = "OUTDATED"
|
|
note = f"Версия ниже безопасной ({safe_ver})"
|
|
elif cmp_res == 0:
|
|
status = "SAFE"
|
|
note = "Соответствует безопасной версии"
|
|
else:
|
|
status = "NEWER"
|
|
note = f"Версия выше эталонной ({safe_ver})"
|
|
|
|
rows.append(
|
|
{
|
|
"container": c.name,
|
|
"image": image_ref,
|
|
"service": service or "-",
|
|
"version": version or "-",
|
|
"status": status,
|
|
"note": note,
|
|
}
|
|
)
|
|
|
|
headers = ["CONTAINER", "IMAGE", "SERVICE", "VERSION", "STATUS", "NOTE"]
|
|
col_widths = {h: len(h) for h in headers}
|
|
|
|
for r in rows:
|
|
col_widths["CONTAINER"] = max(col_widths["CONTAINER"], len(r["container"]))
|
|
col_widths["IMAGE"] = max(col_widths["IMAGE"], len(r["image"]))
|
|
col_widths["SERVICE"] = max(col_widths["SERVICE"], len(str(r["service"])))
|
|
col_widths["VERSION"] = max(col_widths["VERSION"], len(str(r["version"])))
|
|
col_widths["STATUS"] = max(col_widths["STATUS"], len(r["status"]))
|
|
col_widths["NOTE"] = max(col_widths["NOTE"], len(r["note"]))
|
|
|
|
header_line = " ".join(h.ljust(col_widths[h]) for h in headers)
|
|
sep_line = " ".join("-" * col_widths[h] for h in headers)
|
|
print(header_line)
|
|
print(sep_line)
|
|
|
|
for r in rows:
|
|
line = " ".join(
|
|
[
|
|
r["container"].ljust(col_widths["CONTAINER"]),
|
|
r["image"].ljust(col_widths["IMAGE"]),
|
|
str(r["service"]).ljust(col_widths["SERVICE"]),
|
|
str(r["version"]).ljust(col_widths["VERSION"]),
|
|
r["status"].ljust(col_widths["STATUS"]),
|
|
r["note"].ljust(col_widths["NOTE"]),
|
|
]
|
|
)
|
|
print(line)
|
|
|
|
total = len(rows)
|
|
outdated = sum(1 for r in rows if r["status"] == "OUTDATED")
|
|
safe = sum(1 for r in rows if r["status"] == "SAFE")
|
|
unknown = sum(1 for r in rows if r["status"] == "UNKNOWN")
|
|
print()
|
|
print(f"Всего контейнеров: {total}")
|
|
print(f"Безопасных: {safe}")
|
|
print(f"Устаревших: {outdated}")
|
|
print(f"Неопределённых: {unknown}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|