harbor 接入 trivy 漏洞扫描, 用到的开源工具 harbor-scanner-trivy

安装

需要依赖 Redis, 先安装 Redis

安装 trivy

trivy 是扫描核心组件, 需要安装,参考 trivy 官网安装文档

https://aquasecurity.github.io/trivy/v0.41/getting-started/installation/

1
2
3
4
5
6
7
8
9
10
RELEASE_VERSION=$(grep -Po '(?<=VERSION_ID=")[0-9]' /etc/os-release)
cat << EOF | sudo tee -a /etc/yum.repos.d/trivy.repo
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$RELEASE_VERSION/\$basearch/
gpgcheck=1
enabled=1
gpgkey=https://aquasecurity.github.io/trivy-repo/rpm/public.key
EOF
sudo yum -y install trivy

trivy 首次运行会从 github 下载漏洞数据库,需要确保机器可以连接 GitHub,执行 trivy image --download-db-only 会下载 db,db 数据存储在 ~/.cache/trivy

安装 scanner-trivy

开源地址:https://github.com/aquasecurity/harbor-scanner-trivy

scanner-trivy 是通过环境变量读取配置

启动命令:

1
SCANNER_API_SERVER_ADDR=:8181 SCANNER_REDIS_URL=redis://localhost:6379 ./scanner-trivy

或者通过 supervisor 运行

1
2
3
4
5
6
7
8
9
10
11
[program:trivy]
numprocs=1
user=root
command=/data/server/trivy/scanner-trivy
directory=/data/server/trivy/
redirect_stderr=true
stdout_logfile=/data/logs/trivy.log
autostart=true
autorestart=true
startsecs=10
environment=SCANNER_API_SERVER_ADDR=:8181,SCANNER_REDIS_URL=redis://localhost:6379

在 Harbor 里使用

harbor 系统管理 审查服务 扫描器 里面添加 trivy 地址:

trivy扫描器

然后就可以正常扫描镜像了

扫描结果|776

集成到其他地方

trivy 可以支持 json 输出,可以自定义输出的格式,比如生成 html:

1
2
3
4
# 下载一个官方模板
wget https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/html.tpl

trivy image --format template --template "@./html.tpl" -o report.html nginx:latest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trivy 漏洞扫描报告</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
background: #f0f2f5;
color: #1a1a2e;
line-height: 1.5;
}

.container {
max-width: 1400px;
margin: 0 auto;
padding: 24px;
}

/* 头部卡片 */
.header {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 20px;
padding: 32px;
margin-bottom: 24px;
color: white;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}

.header h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 12px;
}

.header .subtitle {
opacity: 0.8;
font-size: 14px;
}

/* 统计卡片网格 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 32px;
}

.stat-card {
background: white;
border-radius: 16px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
transition: transform 0.2s;
}

.stat-card:hover {
transform: translateY(-2px);
}

.stat-number {
font-size: 32px;
font-weight: 700;
margin-bottom: 8px;
}

.stat-label {
color: #666;
font-size: 14px;
}

/* 严重程度颜色 */
.severity-CRITICAL { background: #dc2626; color: white; }
.severity-HIGH { background: #f97316; color: white; }
.severity-MEDIUM { background: #eab308; color: #1a1a2e; }
.severity-LOW { background: #22c55e; color: white; }
.severity-UNKNOWN { background: #9ca3af; color: white; }

/* 表格容器 */
.table-wrapper {
background: white;
border-radius: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
overflow-x: auto;
margin-bottom: 24px;
}

.table-wrapper h2 {
font-size: 20px;
font-weight: 600;
padding: 20px 24px 12px 24px;
border-bottom: 1px solid #e5e7eb;
}

.target-info {
font-size: 14px;
color: #6b7280;
font-weight: normal;
margin-left: 12px;
}

table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}

th {
background: #f9fafb;
padding: 14px 12px;
text-align: left;
font-weight: 600;
color: #374151;
border-bottom: 1px solid #e5e7eb;
position: sticky;
top: 0;
}

td {
padding: 12px;
border-bottom: 1px solid #f0f0f0;
vertical-align: top;
}

tr:hover {
background: #fafafa;
}

/* 漏洞编号链接 */
.vuln-link {
color: #2563eb;
text-decoration: none;
font-weight: 500;
}

.vuln-link:hover {
text-decoration: underline;
}

/* 严重程度徽章 */
.severity-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-align: center;
min-width: 70px;
}

/* 修复建议样式 */
.fix-available {
color: #10b981;
font-weight: 500;
}

.fix-not-available {
color: #f97316;
}

/* 页脚 */
.footer {
text-align: center;
padding: 20px;
color: #6b7280;
font-size: 12px;
}

@media (max-width: 768px) {
.container { padding: 16px; }
th, td { padding: 8px; font-size: 11px; }
.stat-number { font-size: 24px; }
}
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<div class="header">
<h1>🐳 Trivy 容器镜像安全扫描报告</h1>
<div class="subtitle">扫描时间: {{ now | date "2006-01-02 15:04:05" }}</div>
</div>

{{ $totalCritical := 0 }}
{{ $totalHigh := 0 }}
{{ $totalMedium := 0 }}
{{ $totalLow := 0 }}
{{ $totalUnknown := 0 }}

{{ range . }}
{{ range .Vulnerabilities }}
{{ if eq .Severity "CRITICAL" }}{{ $totalCritical = add $totalCritical 1 }}{{ end }}
{{ if eq .Severity "HIGH" }}{{ $totalHigh = add $totalHigh 1 }}{{ end }}
{{ if eq .Severity "MEDIUM" }}{{ $totalMedium = add $totalMedium 1 }}{{ end }}
{{ if eq .Severity "LOW" }}{{ $totalLow = add $totalLow 1 }}{{ end }}
{{ if eq .Severity "UNKNOWN" }}{{ $totalUnknown = add $totalUnknown 1 }}{{ end }}
{{ end }}
{{ end }}

<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number" style="color: #dc2626;">{{ $totalCritical }}</div>
<div class="stat-label">🔴 严重 (CRITICAL)</div>
</div>
<div class="stat-card">
<div class="stat-number" style="color: #f97316;">{{ $totalHigh }}</div>
<div class="stat-label">🟠 高危 (HIGH)</div>
</div>
<div class="stat-card">
<div class="stat-number" style="color: #eab308;">{{ $totalMedium }}</div>
<div class="stat-label">🟡 中危 (MEDIUM)</div>
</div>
<div class="stat-card">
<div class="stat-number" style="color: #22c55e;">{{ $totalLow }}</div>
<div class="stat-label">🟢 低危 (LOW)</div>
</div>
</div>

<!-- 漏洞详情表格 -->
<div class="table-wrapper">
<h2>📋 漏洞详情列表</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>漏洞编号 (CVE)</th>
<th>软件包 / 组件</th>
<th>已安装版本</th>
<th>严重程度</th>
<th>文件位置</th>
<th>修复建议</th>
</tr>
</thead>
<tbody>
{{ $idx := 0 }}
{{ range $result := . }}
{{ range $vuln := $result.Vulnerabilities }}
{{ $idx = add $idx 1 }}
<tr>
<td style="color:#6b7280;">{{ $idx }}</td>
<td><a href="{{ $vuln.PrimaryURL }}" target="_blank" class="vuln-link">{{ $vuln.VulnerabilityID }}</a></td>
<td><code>{{ $vuln.PkgName }}</code></td>
<td><code>{{ $vuln.InstalledVersion }}</code></td>
<td><span class="severity-badge severity-{{ $vuln.Severity }}">{{ $vuln.Severity }}</span></td>
<td><code style="font-size:11px; word-break:break-all;">{{ if $vuln.PkgPath }}{{ $vuln.PkgPath }}{{ else }}-{{ end }}</code></td>
<td>{{ if $vuln.FixedVersion }}<span class="fix-available">✅ 升级至 {{ $vuln.FixedVersion }}</span>{{ else }}<span class="fix-not-available">⚠️ 暂无可用修复版本</span>{{ end }}</td>
</tr>
{{ end }}
{{ end }}
{{ if eq $idx 0 }}
<tr>
<td colspan="7" style="text-align: center; padding: 48px;">
🎉 未发现任何漏洞,镜像安全!
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>

<!-- 页脚 -->
<div class="footer">
<p>报告生成时间: {{ now | date "2006-01-02 15:04:05" }} | 使用 Trivy 扫描</p>
</div>
</div>
</body>
</html>

其他

使用国内镜像更新

借助 oras 来拉取镜像, 需要先安装 oras

1
2
3
4
trivy image --download-db-only --db-repository public.ecr.aws/aquasecurity/trivy-db

# 如果是扫描 Java 项目,还需更新 Java 数据库
trivy image --download-java-db-only --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db

数据库文件默认位置:
Linux: ~/.cache/trivy
macOS: ~/Library/Caches/trivy

使用 crontab 执行此脚本, 一天一次即可。