create-dwy 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +1 -1
- package/package.json +2 -2
- package/src/create.js +61 -37
- package/src/sync.js +213 -53
- package/templates/claude-global/CLAUDE.md +92 -0
- package/templates/claude-global/rules/dwy-android-core.md +610 -0
- package/templates/claude-global/rules/dwy-api-design.md +110 -0
- package/templates/claude-global/rules/dwy-db-migration.md +46 -0
- package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
- package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
- package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
- package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
- package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
- package/templates/claude-global/rules/dwy-git-security.md +53 -0
- package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
- package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
- package/templates/claude-global/rules/dwy-project-structure.md +181 -0
- package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +314 -357
- package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
- package/templates/claude-global/rules/dwy-python-orm.md +61 -0
- package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +116 -79
- package/templates/claude-global/rules/dwy-python-testing.md +53 -0
- package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
- package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
- package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
- package/templates/claude-global/rules/dwy-vue-api.md +146 -0
- package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +361 -152
- package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
- package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
- package/templates/claude-global/settings.json +20 -0
- package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
- package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
- package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
- package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
- package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
- package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
- package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
- package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
- package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
- package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
- package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
- package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
- package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
- package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
- package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
- package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
- package/templates/claude-global/skills/dwy-publish/SKILL.md +135 -0
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
- package/templates/project/backend/.dwy.hbs +5 -0
- package/templates/project/backend/.python-version +1 -0
- package/templates/project/backend/CLAUDE.md.hbs +40 -0
- package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
- package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
- package/templates/project/backend/deploy.sh.hbs +18 -0
- package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
- package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
- package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
- package/templates/project/backend/src/app/database.py +10 -0
- package/templates/project/backend/src/app/dependencies.py +5 -0
- package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
- package/templates/project/backend/src/app/models/base.py +5 -0
- package/templates/project/web/CLAUDE.md.hbs +29 -36
- package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
- package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
- package/templates/project/web/apps/web/src/App.vue +9 -0
- package/templates/project/web/apps/web/src/api/client.ts +5 -0
- package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
- package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
- package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
- package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
- package/templates/project/web/apps/web/src/style.css +2 -0
- package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
- package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
- package/templates/project/web/apps/web/tsconfig.app.json +18 -0
- package/templates/project/web/apps/web/tsconfig.json +6 -0
- package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
- package/templates/project/web/package.json.hbs +16 -0
- package/templates/project/web/packages/.gitkeep +0 -0
- package/templates/project/web/pnpm-workspace.yaml +3 -0
- package/templates/claude-global/rules/agent-team-execution.md +0 -138
- package/templates/claude-global/rules/agent-team-patterns.md +0 -157
- package/templates/claude-global/rules/git-security.md +0 -144
- package/templates/claude-global/skills/dwy-auto-sync/SKILL.md +0 -52
- package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
- package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
- package/templates/project/web/.claude/commands/deploy.md +0 -4
- package/templates/project/web/backend/src/app/models/base.py +0 -5
- package/templates/project/web/deploy.sh.hbs +0 -113
- package/templates/project/web/frontend/src/App.vue +0 -3
- package/templates/project/web/frontend/src/api/client.ts +0 -13
- package/templates/project/web/frontend/src/style.css +0 -1
- package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
- package/templates/project/web/frontend/tsconfig.app.json +0 -14
- package/templates/project/web/frontend/tsconfig.json +0 -4
- /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
- /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
- /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
- /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
- /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
- /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
- /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/main.ts +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
查询 DockerHub 上某镜像的版本列表,自动剔除 latest / rc / beta / alpha / preview / nightly
|
|
4
|
+
等不稳定标签和浮动 tag,并推荐使用「最新稳定 minor 的上一个 minor」(N-1)。
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
python3 query_dockerhub.py <image> [--namespace library] [--top 20]
|
|
8
|
+
|
|
9
|
+
示例:
|
|
10
|
+
python3 query_dockerhub.py nginx
|
|
11
|
+
python3 query_dockerhub.py postgres
|
|
12
|
+
python3 query_dockerhub.py mycompany/api --namespace mycompany
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
import urllib.request
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from urllib.error import HTTPError, URLError
|
|
23
|
+
|
|
24
|
+
DOCKERHUB_API = "https://hub.docker.com/v2/repositories/{namespace}/{image}/tags"
|
|
25
|
+
|
|
26
|
+
UNSTABLE_KEYWORDS = (
|
|
27
|
+
"rc", "beta", "alpha", "preview", "nightly", "dev", "snapshot",
|
|
28
|
+
"experimental", "edge", "canary", "next", "unstable",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
FLOATING_TAGS = {
|
|
32
|
+
"latest", "stable", "mainline", "current", "release",
|
|
33
|
+
"lts", "rolling", "main", "master", "head",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
SEMVER_RE = re.compile(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:[.\-+](.+))?$")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fetch_tags(namespace: str, image: str, page_size: int = 100, max_pages: int = 5):
|
|
40
|
+
"""从 DockerHub API 拉取 tags,返回原始列表。"""
|
|
41
|
+
url = DOCKERHUB_API.format(namespace=namespace, image=image) + f"?page_size={page_size}"
|
|
42
|
+
tags = []
|
|
43
|
+
page = 1
|
|
44
|
+
while url and page <= max_pages:
|
|
45
|
+
try:
|
|
46
|
+
req = urllib.request.Request(url, headers={"User-Agent": "dwy-docker-image-skill/1.0"})
|
|
47
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
48
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
49
|
+
except HTTPError as e:
|
|
50
|
+
if e.code == 404:
|
|
51
|
+
print(f"错误:DockerHub 上找不到 {namespace}/{image}", file=sys.stderr)
|
|
52
|
+
sys.exit(2)
|
|
53
|
+
raise
|
|
54
|
+
except URLError as e:
|
|
55
|
+
print(f"错误:访问 DockerHub 失败:{e}", file=sys.stderr)
|
|
56
|
+
sys.exit(3)
|
|
57
|
+
|
|
58
|
+
tags.extend(data.get("results", []))
|
|
59
|
+
url = data.get("next")
|
|
60
|
+
page += 1
|
|
61
|
+
return tags
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_version(tag: str):
|
|
65
|
+
"""
|
|
66
|
+
解析 tag 中的版本号(major, minor, patch, suffix)。
|
|
67
|
+
|
|
68
|
+
返回 None 表示这个 tag 不能解析为版本号(比如 'alpine'、'bookworm')。
|
|
69
|
+
"""
|
|
70
|
+
cleaned = tag.lstrip("v")
|
|
71
|
+
m = SEMVER_RE.match(cleaned)
|
|
72
|
+
if not m:
|
|
73
|
+
return None
|
|
74
|
+
major = int(m.group(1))
|
|
75
|
+
minor = int(m.group(2)) if m.group(2) else 0
|
|
76
|
+
patch = int(m.group(3)) if m.group(3) else 0
|
|
77
|
+
suffix = m.group(4) or ""
|
|
78
|
+
return (major, minor, patch, suffix)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_stable(tag_name: str) -> bool:
|
|
82
|
+
"""判断 tag 是否为稳定版本。"""
|
|
83
|
+
name = tag_name.lower()
|
|
84
|
+
if name in FLOATING_TAGS:
|
|
85
|
+
return False
|
|
86
|
+
for kw in UNSTABLE_KEYWORDS:
|
|
87
|
+
if re.search(rf"[\-._]{kw}\b", name) or name.endswith(kw) or name.startswith(kw + "-"):
|
|
88
|
+
return False
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def has_only_version(tag_name: str) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
tag 是否仅包含版本号且至少包含 major.minor 两段。
|
|
95
|
+
|
|
96
|
+
单段数字 tag(如 '1'、'17')虽然看起来固定,实际是浮动 tag(指向该 major
|
|
97
|
+
系列最新 patch),不能用作固定版本,必须排除。
|
|
98
|
+
"""
|
|
99
|
+
return bool(re.match(r"^v?\d+\.\d+(\.\d+)?$", tag_name))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main():
|
|
103
|
+
parser = argparse.ArgumentParser(description="查询 DockerHub 镜像版本,推荐 N-1 minor")
|
|
104
|
+
parser.add_argument("image", help="镜像名,如 nginx、postgres")
|
|
105
|
+
parser.add_argument("--namespace", default="library", help="DockerHub 命名空间,默认 library")
|
|
106
|
+
parser.add_argument("--top", type=int, default=20, help="显示最近 N 个 tag,默认 20")
|
|
107
|
+
args = parser.parse_args()
|
|
108
|
+
|
|
109
|
+
print(f"查询 DockerHub: {args.namespace}/{args.image} ...\n", file=sys.stderr)
|
|
110
|
+
tags = fetch_tags(args.namespace, args.image)
|
|
111
|
+
if not tags:
|
|
112
|
+
print(f"未找到 {args.namespace}/{args.image} 的任何 tag", file=sys.stderr)
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
# 过滤:稳定 + 纯版本号
|
|
116
|
+
pure_version_tags = []
|
|
117
|
+
for t in tags:
|
|
118
|
+
name = t.get("name", "")
|
|
119
|
+
if not is_stable(name):
|
|
120
|
+
continue
|
|
121
|
+
if not has_only_version(name):
|
|
122
|
+
continue
|
|
123
|
+
ver = parse_version(name)
|
|
124
|
+
if ver is None:
|
|
125
|
+
continue
|
|
126
|
+
pure_version_tags.append({
|
|
127
|
+
"name": name,
|
|
128
|
+
"version": ver,
|
|
129
|
+
"last_updated": t.get("last_updated", ""),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if not pure_version_tags:
|
|
133
|
+
print("未找到符合规范的稳定版本 tag。请手动确认。", file=sys.stderr)
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
|
|
136
|
+
# 按 (major, minor) 分组,每组取最大 patch
|
|
137
|
+
minor_groups = defaultdict(list)
|
|
138
|
+
for item in pure_version_tags:
|
|
139
|
+
major, minor, _patch, _suffix = item["version"]
|
|
140
|
+
minor_groups[(major, minor)].append(item)
|
|
141
|
+
|
|
142
|
+
# 每组按版本号排序,取最大 patch 作为该 minor 的代表
|
|
143
|
+
minor_latest = {}
|
|
144
|
+
for key, items in minor_groups.items():
|
|
145
|
+
items.sort(key=lambda x: x["version"], reverse=True)
|
|
146
|
+
minor_latest[key] = items[0]
|
|
147
|
+
|
|
148
|
+
# 按 (major, minor) 倒序排列
|
|
149
|
+
sorted_minors = sorted(minor_latest.keys(), reverse=True)
|
|
150
|
+
|
|
151
|
+
if not sorted_minors:
|
|
152
|
+
print("无法识别任何 (major, minor) 系列", file=sys.stderr)
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
|
|
155
|
+
latest_minor = sorted_minors[0]
|
|
156
|
+
latest_item = minor_latest[latest_minor]
|
|
157
|
+
|
|
158
|
+
# N-1:跳过仅有少量 patch 的快速被弃用版本,找真正的"上一个 minor"
|
|
159
|
+
n_minus_1_item = None
|
|
160
|
+
if len(sorted_minors) >= 2:
|
|
161
|
+
n_minus_1_minor = sorted_minors[1]
|
|
162
|
+
n_minus_1_item = minor_latest[n_minus_1_minor]
|
|
163
|
+
|
|
164
|
+
# 输出
|
|
165
|
+
print("=" * 60)
|
|
166
|
+
print(f"镜像: {args.namespace}/{args.image}")
|
|
167
|
+
print("=" * 60)
|
|
168
|
+
major, minor, patch, _ = latest_item["version"]
|
|
169
|
+
print(f"最新稳定 minor: {major}.{minor}")
|
|
170
|
+
print(f" 最新 patch: {latest_item['name']} ({format_date(latest_item['last_updated'])})")
|
|
171
|
+
print()
|
|
172
|
+
|
|
173
|
+
if n_minus_1_item:
|
|
174
|
+
n_major, n_minor, n_patch, _ = n_minus_1_item["version"]
|
|
175
|
+
print(f"✅ 推荐 (N-1 minor): {args.image}:{n_minus_1_item['name']}")
|
|
176
|
+
print(f" {n_major}.{n_minor} 系列最新 patch,发布于 {format_date(n_minus_1_item['last_updated'])}")
|
|
177
|
+
else:
|
|
178
|
+
print("⚠️ 仅找到一个 minor 系列,无法推荐 N-1,请手动确认是否可降级 major")
|
|
179
|
+
print()
|
|
180
|
+
|
|
181
|
+
print(f"最近 {args.top} 个稳定 tag(每个 minor 取最新 patch):")
|
|
182
|
+
n_minus_1_key = sorted_minors[1] if len(sorted_minors) >= 2 else None
|
|
183
|
+
for key in sorted_minors[:args.top]:
|
|
184
|
+
item = minor_latest[key]
|
|
185
|
+
marker = ""
|
|
186
|
+
if key == latest_minor:
|
|
187
|
+
marker = " ← 最新"
|
|
188
|
+
elif key == n_minus_1_key:
|
|
189
|
+
marker = " ← 推荐 (N-1)"
|
|
190
|
+
print(f" {item['name']:20s} {format_date(item['last_updated'])}{marker}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def format_date(iso: str) -> str:
|
|
194
|
+
if not iso:
|
|
195
|
+
return "-"
|
|
196
|
+
try:
|
|
197
|
+
dt = datetime.fromisoformat(iso.replace("Z", "+00:00"))
|
|
198
|
+
return dt.strftime("%Y-%m-%d")
|
|
199
|
+
except ValueError:
|
|
200
|
+
return iso[:10]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
main()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 扫描项目中所有 Docker 镜像引用,输出违规清单(latest / 浮动 tag / 缺 tag)。
|
|
3
|
+
#
|
|
4
|
+
# 用法:
|
|
5
|
+
# bash scan_images.sh [project-path]
|
|
6
|
+
#
|
|
7
|
+
# 不传路径则扫描当前目录。脚本只读不写,仅输出报告,不修改任何文件。
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
ROOT="${1:-.}"
|
|
12
|
+
|
|
13
|
+
if [ ! -d "$ROOT" ]; then
|
|
14
|
+
echo "错误:路径不存在或不是目录:$ROOT" >&2
|
|
15
|
+
exit 2
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# 浮动 tag 黑名单(不含版本号的纯字符串 tag)
|
|
19
|
+
FLOATING_TAGS_RE='^(latest|stable|mainline|current|release|lts|rolling|main|master|head|edge|nightly|alpine|slim|bookworm|bullseye|buster|jammy|focal|noble|trixie)$'
|
|
20
|
+
|
|
21
|
+
# 临时输出文件
|
|
22
|
+
TMP_OUT="$(mktemp)"
|
|
23
|
+
trap 'rm -f "$TMP_OUT"' EXIT
|
|
24
|
+
|
|
25
|
+
count_critical=0
|
|
26
|
+
count_high=0
|
|
27
|
+
count_low=0
|
|
28
|
+
|
|
29
|
+
# 工具函数:分级记录
|
|
30
|
+
record() {
|
|
31
|
+
local level="$1"
|
|
32
|
+
local file="$2"
|
|
33
|
+
local line="$3"
|
|
34
|
+
local content="$4"
|
|
35
|
+
local hint="$5"
|
|
36
|
+
printf "[%s] %s:%s\n %s\n → %s\n\n" "$level" "$file" "$line" "$content" "$hint" >> "$TMP_OUT"
|
|
37
|
+
case "$level" in
|
|
38
|
+
critical) count_critical=$((count_critical + 1)) ;;
|
|
39
|
+
high) count_high=$((count_high + 1)) ;;
|
|
40
|
+
low) count_low=$((count_low + 1)) ;;
|
|
41
|
+
esac
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# 解析 image 字符串,返回违规级别和提示
|
|
45
|
+
# 输入:image_ref(如 nginx:latest、postgres、redis:7)
|
|
46
|
+
# 输出:通过 record 函数记录
|
|
47
|
+
analyze_image() {
|
|
48
|
+
local image_ref="$1"
|
|
49
|
+
local file="$2"
|
|
50
|
+
local line="$3"
|
|
51
|
+
local raw_line="$4"
|
|
52
|
+
|
|
53
|
+
# 去掉 digest 部分(@sha256:...)参与 tag 分析
|
|
54
|
+
local without_digest="${image_ref%@sha256:*}"
|
|
55
|
+
|
|
56
|
+
# 拆分 image 和 tag
|
|
57
|
+
local image_part tag_part
|
|
58
|
+
if [[ "$without_digest" == *:* ]]; then
|
|
59
|
+
# 注意 registry 可能含端口(如 registry.example.com:5000/image:tag)
|
|
60
|
+
# 用最后一个冒号分隔
|
|
61
|
+
image_part="${without_digest%:*}"
|
|
62
|
+
tag_part="${without_digest##*:}"
|
|
63
|
+
else
|
|
64
|
+
image_part="$without_digest"
|
|
65
|
+
tag_part=""
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# 1. 缺 tag(等同于 latest)
|
|
69
|
+
if [ -z "$tag_part" ]; then
|
|
70
|
+
record "critical" "$file" "$line" "$raw_line" "镜像 $image_part 未指定 tag(等同于 latest),改为固定版本如 $image_part:<N-1-minor>"
|
|
71
|
+
return
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# 2. 显式 latest
|
|
75
|
+
if [ "$tag_part" = "latest" ]; then
|
|
76
|
+
record "critical" "$file" "$line" "$raw_line" "禁止使用 latest,改为固定版本如 $image_part:<N-1-minor>(用 query_dockerhub.py 查推荐版本)"
|
|
77
|
+
return
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# 3. 浮动 tag(不含数字)
|
|
81
|
+
if [[ "$tag_part" =~ $FLOATING_TAGS_RE ]]; then
|
|
82
|
+
record "high" "$file" "$line" "$raw_line" "tag '$tag_part' 是浮动 tag,会随上游更新漂移,改为带版本号的具体 tag"
|
|
83
|
+
return
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# 4. 单段数字(如 redis:7、postgres:17)
|
|
87
|
+
if [[ "$tag_part" =~ ^v?[0-9]+$ ]]; then
|
|
88
|
+
record "high" "$file" "$line" "$raw_line" "单段版本号 '$tag_part' 实际是浮动 tag(指向该 major 最新),钉到 minor 或 patch"
|
|
89
|
+
return
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# 5. 看起来固定但没有 digest(仅作为低优先级提醒)
|
|
93
|
+
if [[ "$tag_part" =~ ^v?[0-9]+\.[0-9]+ ]] && [[ ! "$image_ref" =~ @sha256: ]]; then
|
|
94
|
+
# 不是所有 image 都需要 digest,这里只对常见生产关键服务提醒
|
|
95
|
+
if [[ "$image_part" =~ (postgres|mysql|mariadb|mongo|redis|kafka|rabbitmq|elasticsearch|nginx|envoyproxy)$ ]]; then
|
|
96
|
+
record "low" "$file" "$line" "$raw_line" "生产关键服务建议追加 @sha256:... digest,让 image 完全不可篡改"
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# 扫描 Dockerfile 风格文件
|
|
102
|
+
scan_dockerfiles() {
|
|
103
|
+
while IFS= read -r -d '' f; do
|
|
104
|
+
local lineno=0
|
|
105
|
+
while IFS= read -r line; do
|
|
106
|
+
lineno=$((lineno + 1))
|
|
107
|
+
# 匹配 FROM 指令,去掉 AS 别名
|
|
108
|
+
if [[ "$line" =~ ^[[:space:]]*FROM[[:space:]]+([^[:space:]]+) ]]; then
|
|
109
|
+
local image_ref="${BASH_REMATCH[1]}"
|
|
110
|
+
# 跳过参数化的 FROM(如 FROM ${BASE_IMAGE})
|
|
111
|
+
if [[ "$image_ref" =~ ^\$\{ ]] || [[ "$image_ref" =~ ^\$[A-Z_] ]]; then
|
|
112
|
+
continue
|
|
113
|
+
fi
|
|
114
|
+
# 跳过 scratch
|
|
115
|
+
if [ "$image_ref" = "scratch" ]; then
|
|
116
|
+
continue
|
|
117
|
+
fi
|
|
118
|
+
analyze_image "$image_ref" "$f" "$lineno" "$(echo "$line" | sed 's/^[[:space:]]*//')"
|
|
119
|
+
fi
|
|
120
|
+
done < "$f"
|
|
121
|
+
done < <(find "$ROOT" \
|
|
122
|
+
\( -name "Dockerfile" -o -name "Dockerfile.*" -o -name "*.Dockerfile" \) \
|
|
123
|
+
-not -path "*/node_modules/*" \
|
|
124
|
+
-not -path "*/.git/*" \
|
|
125
|
+
-print0)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# 扫描 yaml 风格文件(compose / k8s / CI)
|
|
129
|
+
scan_yaml_files() {
|
|
130
|
+
while IFS= read -r -d '' f; do
|
|
131
|
+
local lineno=0
|
|
132
|
+
while IFS= read -r line; do
|
|
133
|
+
lineno=$((lineno + 1))
|
|
134
|
+
# 匹配 image: xxx 或 image: "xxx"
|
|
135
|
+
if [[ "$line" =~ ^[[:space:]]*image:[[:space:]]*[\"\']?([^[:space:]\"\'#]+) ]]; then
|
|
136
|
+
local image_ref="${BASH_REMATCH[1]}"
|
|
137
|
+
# 跳过模板变量
|
|
138
|
+
if [[ "$image_ref" =~ ^\$\{ ]] || [[ "$image_ref" =~ ^\$[A-Z_] ]] || [[ "$image_ref" =~ \{\{ ]]; then
|
|
139
|
+
continue
|
|
140
|
+
fi
|
|
141
|
+
analyze_image "$image_ref" "$f" "$lineno" "$(echo "$line" | sed 's/^[[:space:]]*//')"
|
|
142
|
+
fi
|
|
143
|
+
done < "$f"
|
|
144
|
+
done < <(find "$ROOT" \
|
|
145
|
+
\( -name "docker-compose*.yml" -o -name "docker-compose*.yaml" \
|
|
146
|
+
-o -name "compose.yml" -o -name "compose.yaml" \
|
|
147
|
+
-o -name "*.yaml" -o -name "*.yml" \) \
|
|
148
|
+
-not -path "*/node_modules/*" \
|
|
149
|
+
-not -path "*/.git/*" \
|
|
150
|
+
-not -path "*/dist/*" \
|
|
151
|
+
-not -path "*/build/*" \
|
|
152
|
+
-print0)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
echo "扫描目录: $ROOT" >&2
|
|
156
|
+
echo "" >&2
|
|
157
|
+
|
|
158
|
+
scan_dockerfiles
|
|
159
|
+
scan_yaml_files
|
|
160
|
+
|
|
161
|
+
# 输出结果
|
|
162
|
+
total=$((count_critical + count_high + count_low))
|
|
163
|
+
|
|
164
|
+
if [ "$total" -eq 0 ]; then
|
|
165
|
+
echo "✅ 未发现违规:所有镜像都已固定到具体版本。"
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
cat "$TMP_OUT"
|
|
170
|
+
|
|
171
|
+
echo "----------------------------------------"
|
|
172
|
+
echo "汇总: $total 项问题"
|
|
173
|
+
echo " critical: $count_critical (latest / 缺 tag)"
|
|
174
|
+
echo " high: $count_high (浮动 tag / 单段版本号)"
|
|
175
|
+
echo " low: $count_low (建议补 digest)"
|
|
176
|
+
echo ""
|
|
177
|
+
echo "下一步:"
|
|
178
|
+
echo " 1. 用 query_dockerhub.py <image> 查每个违规镜像的推荐版本"
|
|
179
|
+
echo " 2. 用 Edit 工具逐项修复,不要批量替换(不同环境可能用不同 tag 变体)"
|
|
180
|
+
|
|
181
|
+
# 有 critical 或 high 时返回非零,方便 CI 集成
|
|
182
|
+
if [ "$count_critical" -gt 0 ] || [ "$count_high" -gt 0 ]; then
|
|
183
|
+
exit 1
|
|
184
|
+
fi
|
|
185
|
+
exit 0
|