create-dwy 0.5.1 → 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.
Files changed (156) hide show
  1. package/bin/index.js +1 -1
  2. package/package.json +2 -2
  3. package/src/create.js +61 -37
  4. package/src/sync.js +212 -61
  5. package/templates/claude-global/CLAUDE.md +92 -0
  6. package/templates/claude-global/rules/dwy-android-core.md +610 -0
  7. package/templates/claude-global/rules/dwy-api-design.md +110 -0
  8. package/templates/claude-global/rules/dwy-db-migration.md +46 -0
  9. package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
  10. package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
  11. package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
  12. package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
  13. package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
  14. package/templates/claude-global/rules/dwy-git-security.md +53 -0
  15. package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
  16. package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
  17. package/templates/claude-global/rules/dwy-project-structure.md +181 -0
  18. package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +292 -358
  19. package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
  20. package/templates/claude-global/rules/dwy-python-orm.md +61 -0
  21. package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +99 -72
  22. package/templates/claude-global/rules/dwy-python-testing.md +53 -0
  23. package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
  24. package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
  25. package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
  26. package/templates/claude-global/rules/dwy-vue-api.md +146 -0
  27. package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +338 -152
  28. package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
  29. package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
  30. package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
  31. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
  32. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
  33. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
  34. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
  35. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
  36. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
  37. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
  38. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
  39. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
  40. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
  41. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
  42. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
  43. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
  44. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
  45. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
  46. package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
  47. package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
  48. package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
  49. package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
  50. package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
  51. package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
  52. package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
  53. package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
  54. package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
  55. package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
  56. package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
  57. package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
  58. package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
  59. package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
  60. package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
  61. package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
  62. package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
  63. package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
  64. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
  65. package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
  66. package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
  67. package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
  68. package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
  69. package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
  70. package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
  71. package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
  72. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
  73. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
  74. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
  75. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
  76. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
  77. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
  78. package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
  79. package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
  80. package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
  81. package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
  82. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
  83. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
  84. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
  85. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
  86. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
  87. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
  88. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
  89. package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
  90. package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
  91. package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
  92. package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
  93. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
  94. package/templates/project/backend/.dwy.hbs +5 -0
  95. package/templates/project/backend/.python-version +1 -0
  96. package/templates/project/backend/CLAUDE.md.hbs +40 -0
  97. package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
  98. package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
  99. package/templates/project/backend/deploy.sh.hbs +18 -0
  100. package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
  101. package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
  102. package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
  103. package/templates/project/backend/src/app/database.py +10 -0
  104. package/templates/project/backend/src/app/dependencies.py +5 -0
  105. package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
  106. package/templates/project/backend/src/app/models/base.py +5 -0
  107. package/templates/project/web/CLAUDE.md.hbs +29 -36
  108. package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
  109. package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
  110. package/templates/project/web/apps/web/src/App.vue +9 -0
  111. package/templates/project/web/apps/web/src/api/client.ts +5 -0
  112. package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
  113. package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
  114. package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
  115. package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
  116. package/templates/project/web/apps/web/src/style.css +2 -0
  117. package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
  118. package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
  119. package/templates/project/web/apps/web/tsconfig.app.json +18 -0
  120. package/templates/project/web/apps/web/tsconfig.json +6 -0
  121. package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
  122. package/templates/project/web/package.json.hbs +16 -0
  123. package/templates/project/web/packages/.gitkeep +0 -0
  124. package/templates/project/web/pnpm-workspace.yaml +3 -0
  125. package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
  126. package/templates/claude-global/rules/agent-team-execution.md +0 -138
  127. package/templates/claude-global/rules/agent-team-patterns.md +0 -157
  128. package/templates/claude-global/rules/git-commit-convention.md +0 -106
  129. package/templates/claude-global/rules/git-security.md +0 -144
  130. package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
  131. package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
  132. package/templates/project/web/.claude/commands/deploy.md +0 -4
  133. package/templates/project/web/backend/src/app/models/base.py +0 -5
  134. package/templates/project/web/deploy.sh.hbs +0 -113
  135. package/templates/project/web/frontend/src/App.vue +0 -3
  136. package/templates/project/web/frontend/src/api/client.ts +0 -13
  137. package/templates/project/web/frontend/src/style.css +0 -1
  138. package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
  139. package/templates/project/web/frontend/tsconfig.app.json +0 -14
  140. package/templates/project/web/frontend/tsconfig.json +0 -4
  141. /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
  142. /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
  143. /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
  144. /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
  145. /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
  146. /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
  147. /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
  148. /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
  149. /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
  150. /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
  151. /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
  152. /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
  153. /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
  154. /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
  155. /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
  156. /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