Repository navigation
Expand file tree
/
Copy pathjustfile
More file actions
679 lines (597 loc) · 23.9 KB
/
Copy pathjustfile
File metadata and controls
679 lines (597 loc) · 23.9 KB
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
set shell := ["bash", "-cu"]
cpus := env("GRATE_CPUS", `nproc --ignore=2`)
memory := env("GRATE_MEMORY", `free -m | awk 'NR == 2 { print int($NF - $NF * 0.25) "m" }'`)
pids_limit := env("GRATE_PIDS_LIMIT", "1024")
network := env("GRATE_NETWORK", "grate")
subnet := env("GRATE_SUBNET", "10.41.0.0/24")
workspace := absolute_path(env("GRATE_WORKSPACE", "workspace"))
llama_model_hf_repo := env("GRATE_LLAMA_MODEL_HF_REPO", "unsloth/Qwen3.5-4B-GGUF")
llama_model_hf_file := env("GRATE_LLAMA_MODEL_HF_FILE", "Qwen3.5-4B-Q4_K_M.gguf")
nft_file := env("GRATE_NFT_FILE", "grate.nft")
cisa_kev_catalog_url := env(
"GRATE_CISA_KEV_CATALOG_URL",
"https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
)
rust_image_version := env(
"GRATE_RUST_IMAGE_VERSION",
"1.96.0-slim-trixie@sha256:3b05f7c617a200c41c3506097f0d15fc193a1c93bfd8f141007b47cac8f95d3c"
)
go_image_version := env(
"GRATE_GO_IMAGE_VERSION",
"1.26.4-trixie@sha256:76a29248dedcd75870e95cbd90cc8cb356db082404ac7d3a5803f276c3ba79c9"
)
llama_server_image_version := env(
"GRATE_LLAMA_SERVER_IMAGE_VERSION",
"server-cuda13-b9894@sha256:9b61117f56447863261f6ae0d15c7d330f219b9fbddeac1ccd336f6fc2b31e81"
)
# List all commands
default:
@just -l -u
_variables:
#!/usr/bin/env bash
echo "cpus: {{ cpus }}"
echo "memory: {{ memory }}"
echo "pids_limit: {{ pids_limit }}"
echo "network: {{ network }}"
echo "workspace: {{ workspace }}"
_log msg *args:
#!/usr/bin/env bash
printf "$(tput setaf 8)▶ {{ msg }}$(tput sgr0)\n" {{ quote(args) }}
_info msg *args:
#!/usr/bin/env bash
printf "$(tput setaf 4)▶ {{ msg }}$(tput sgr0)\n" {{ quote(args) }}
_warn msg *args:
#!/usr/bin/env bash
printf "$(tput setaf 3)▶ {{ msg }}$(tput sgr0)\n" {{ quote(args) }}
_error msg *args:
#!/usr/bin/env bash
printf "$(tput setaf 1)▶ {{ msg }}$(tput sgr0)\n" {{ quote(args) }}
_panic msg *args:
#!/usr/bin/env bash
printf "$(tput bold setaf 1)!!! {{ msg }}$(tput sgr0)\n" {{ quote(args) }}
_critical msg *args:
#!/usr/bin/env bash
printf "$(tput bold setaf 3)!!! {{ msg }}$(tput sgr0)\n" {{ quote(args) }}
_sanitize-filename name suffix='':
@echo "$(tr -d '\n' <<<'{{ name }}' | tr -cs 'A-Za-z0-9_' '_'){{ suffix }}"
_fmt-json path:
#!/usr/bin/env bash
just _info "Formatting {{ path }}..."
tmpfile=$(mktemp)
jq . "{{ path }}" >"$tmpfile"
mv "$tmpfile" "{{ path }}"
_check-file path:
#!/usr/bin/env bash
set -euo pipefail
echo -n "Checking {{ path }}... "
test -r "{{ path }}" || (echo "failed (no file {{ path }} was found)"; exit 1)
echo ok
_check-whitelist:
#!/usr/bin/env bash
just _check-file whitelist.conf
_check-seccomp-json:
#!/usr/bin/env bash
just _check-file /usr/share/containers/seccomp.json
just _check-file seccomp.extra.json
_check bin:
#!/usr/bin/env bash
set -euo pipefail
echo -n "Checking {{ bin }}... "
test -x "$(command -v {{ bin }})" || (echo "failed (no executable {{ bin }} was found)"; exit 1)
echo ok
# Check for requirements to run grate
[group('util')]
check:
@just _info "Checking requirements..."
@just _check podman
@just _check buildah
@just _check nft
@just _check jq
@just _check envsubst
@just _check getent
# ================
# Build
# ================
# Build the unbound DNS server image
[group('build')]
build-unbound:
@just _info "Building unbound image for DNS filtering..."
sudo podman image pull --policy=missing cgr.dev/chainguard/wolfi-base
sudo podman build \
--pull=never \
--tag grate-unbound:latest \
--file Containerfile.unbound
_render-grate-json: _check-whitelist
#!/usr/bin/env bash
just _info "Rendering grate.json with whitelisted domains..."
jq --slurpfile domains <(awk '!/^#/ { print $1 }' whitelist.conf | sed '/^$/d' | sort -u | jq -R .) \
'.network.allow_domain += $domains' grate.json >"{{ workspace }}/grate.json"
# Build grate base image
[group('build')]
[arg('with_debug_tools', long='with-debug-tools', value='yes', help='install useful debug tools (e.g., curl)')]
[arg('with_c_dev_tools', long='with-c-dev-tools', value='yes', help='install C development tools (e.g., gcc)')]
build-base with_debug_tools='no' with_c_dev_tools='no':
@just _render-grate-json
@just _info "Building grate base image..."
@just _log "DEBUG_TOOLS={{ with_debug_tools }}"
@just _log "C_DEV_TOOLS={{ with_c_dev_tools }}"
sudo podman image pull --policy=missing cgr.dev/chainguard/wolfi-base
sudo podman build --pull=never \
--tag grate-base:latest \
--file Containerfile.base \
--build-arg DEBUG_TOOLS={{ with_debug_tools }} \
--build-arg C_DEV_TOOLS={{ with_c_dev_tools }} \
--build-arg RUST_IMAGE_VERSION={{ rust_image_version }}
# Build grate radicle image
[group('build')]
build-radicle:
@just _info "Building grate radicle image..."
sudo podman image exists grate-base:latest || just _build-base
sudo podman build --pull=never --tag grate-radicle:latest --file Containerfile.radicle
# Build grate llama.cpp server image
[group('build')]
build-llama-server:
@just _info "Building grate llama.cpp server image..."
sudo podman image exists grate-base:latest || just _build-base
sudo podman image pull --policy=missing cgr.dev/chainguard/wolfi-base
sudo podman image pull --policy=missing ghcr.io/ggml-org/llama.cpp:{{ llama_server_image_version }}
sudo --preserve-env=HF_TOKEN podman build \
--pull=never \
--tag grate-llama-server:latest \
--file Containerfile.llama-server \
--secret id=HF_TOKEN,env=HF_TOKEN \
--build-arg LLAMA_SERVER_IMAGE_VERSION={{ llama_server_image_version }} \
--build-arg MODEL_HF_REPO={{ llama_model_hf_repo }} \
--build-arg MODEL_HF_FILE={{ llama_model_hf_file }}
# Build grate opencode image
[group('build')]
build-opencode:
@just _info "Building grate opencode image..."
sudo podman image exists grate-base:latest || just _build-base
sudo podman image pull --policy=missing cgr.dev/chainguard/wolfi-base
sudo podman build --pull=never --tag grate-opencode:latest --file Containerfile.opencode
# Build all images: grate-unbound, grate-base, grated-radicle, grated-llama-server, grated-opencode
[group('build')]
[arg('with_debug_tools', long='with-debug-tools', value='yes', help='install useful debug tools (e.g., curl)')]
[arg('with_c_dev_tools', long='with-c-dev-tools', value='yes', help='install C development tools (e.g., gcc)')]
build $with_debug_tools='no' $with_c_dev_tools='no':
#!/usr/bin/env bash
just _info "Building all images..."
just build-unbound
set -- build-base
if [ "$with_debug_tools" = "yes" ]; then
set -- "$@" --with-debug-tools
fi
if [ "$with_c_dev_tools" = "yes" ]; then
set -- "$@" --with-c-dev-tools
fi
just "$@"
just build-radicle
just build-llama-server
just build-opencode
# Clean temporary files
[group('build')]
clean:
rm -fv {{ workspace }}/*.grype.json \
{{ workspace }}/*.syft.json \
{{ workspace }}/iface \
{{ workspace }}/dns \
{{ workspace }}/vars.nft \
{{ workspace }}/unbound.conf \
{{ workspace }}/grate.json
# Run clean and also remove workspace data and built images
[group('build')]
dist-clean: clean
rm -fv {{ workspace }}/cisa-kev-catalog.json
sudo podman images --format '{{{{.Repository}}' "grate-*" | xargs sudo podman rmi
find {{ workspace }}/{base,llama,opencode,radicle} -mindepth 1 ! -name .gitkeep -exec rm -rfv {} +
# ================
# Network
# ================
_ip nth:
#!/usr/bin/env bash
echo $(echo {{ subnet }} | cut -d/ -f1 | cut -d. -f1-3).{{ nth }}
_host ctname:
#!/usr/bin/env bash
echo $(awk '/\<{{ ctname }}\>/ { print $1 }' '{{ workspace }}/hosts')
# Create podman network shared by running containers
[group('network')]
ensure-network:
#!/usr/bin/env bash
set -eux
just _info "Creating podman network..."
if sudo podman network exists "{{ network }}"; then
just _warn "network exists, skipping"
exit 0
fi
sudo podman network rm --force "{{ network }}"
sudo podman network create --disable-dns "{{ network }}" --subnet "{{ subnet }}"
sudo podman network inspect \
--format '{{{{.NetworkInterface}}' \
"{{ network }}" >"{{ workspace }}/iface"
cat <<EOF >"{{ workspace }}/hosts"
$(just _ip 200) grate-llama-server
EOF
# Remove podman network shared by running containers
[group('network')]
remove-network:
@just _info "Deleting podman network..."
sudo podman network rm --force "{{ network }}"
rm -fv "{{ workspace }}/iface"
# --- Unbound ---
_render-unbound-conf: _check-whitelist
#!/usr/bin/env bash
just _info "Rendering unbound.conf..."
export GRATE_SUBNET="{{ subnet }}"
export GRATE_LLAMA_SERVER_IP="$(just _host grate-llama-server)"
envsubst < unbound.conf.tpl >"{{ workspace }}/unbound.conf"
awk '!/^#|^$/ { print "\tlocal-zone: \"" $1 "\" transparent" }' whitelist.conf >>"{{ workspace }}/unbound.conf"
# Start unbound DNS server, if not started
[group('network')]
ensure-dns:
#!/usr/bin/env bash
set -eux
just _info "Starting unbound DNS server..."
sudo podman image exists grate-unbound:latest || just _build-unbound
just _render-unbound-conf
if sudo podman ps --filter name=grate-unbound --filter status=running | tail -n+2 | grep -q .; then
just _warn "dns is already running"
exit 0
fi
sudo podman run --rm --detach \
--name "grate-unbound" \
--uidmap=0:30000:8000 \
--gidmap=0:30000:8000 \
--volume '{{ workspace }}/unbound.conf:/etc/unbound/unbound.conf:Z' \
--network "{{ network }}" \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--cap-add=SETUID \
--cap-add=SETGID \
--cap-add=SYS_CHROOT \
--security-opt=no-new-privileges \
--cpus {{ cpus }} \
--memory {{ memory }} \
--pids-limit {{ pids_limit }} \
grate-unbound:latest
sudo podman inspect \
--format "{{{{range .NetworkSettings.Networks}}{{{{.IPAddress}}{{{{end}}" \
grate-unbound >"{{ workspace }}/dns"
# Stop unbound DNS server
[group('network')]
stop-dns:
@just _info "Stopping unbound DNS server..."
sudo podman stop grate-unbound || true
rm -fv "{{ workspace }}/dns"
# --- nftables ---
_set-nft-vars:
#!/usr/bin/env bash
cat >"{{ workspace }}/vars.nft" <<EOF
table inet grate {
set podman_iface {
type ifname
elements = { "$(cat {{ workspace }}/iface)" }
}
set dns_server {
type ipv4_addr
elements = { "$(cat {{ workspace }}/dns)" }
}
}
EOF
[group('network')]
show-firewall-rules:
@just _info "Showing firewall rules..."
sudo nft list table inet grate || true
# Create firewall rules on the host, using nftables (requires root)
[group('network')]
[arg('verbose', long='verbose', short='v', value='true', help='print loaded rules')]
load-firewall-rules verbose='false': _check-whitelist
@just _info "Loading firewall rules..."
just _set-nft-vars
sudo nft -c -f {{ nft_file }}
sudo nft -f {{ nft_file }}
sed '/^#/d; /^$/d' whitelist.conf | while read -r domain ports; do \
sudo nft -f "{{ workspace }}/vars.nft" insert rule inet grate forward \
iifname @podman_iface \
oifname != @podman_iface \
ip daddr "{ $(getent ahostsv4 $domain | awk '{ print $1 }' | sort -u | paste -sd,) }" \
tcp dport { $(echo $ports | tr ' ' ,) } accept \
comment "\"$domain $ports\""; \
done
[ "{{ verbose }}" = "true" ] && sudo nft list table inet grate || true
# Delete firewall rules on the host, using nftables (requires root)
[group('network')]
unload-firewall-rules:
@just _info "Unloading firewall rules..."
sudo nft delete table inet grate || true
# ================
# Run
# ================
_seccomp-render: _check-seccomp-json
#!/usr/bin/env bash
just _info "Merging default seccomp Podman profile with extra rules..."
jq --argjson syscalls "$(jq -c '[.syscalls[].names[]] | unique' seccomp.extra.json)" '
.syscalls |= (
map(
if .action == "SCMP_ACT_ALLOW" then
.names |= map(
. as $name
| select($syscalls | index($name) | not)
)
else
.
end
)
| map(select(.names | length > 0))
)
' /usr/share/containers/seccomp.json \
>"{{ workspace }}/seccomp.json"
tmpfile=$(mktemp)
jq --argjson syscalls "$(jq -c '.syscalls' seccomp.extra.json)" \
'.syscalls += $syscalls' "{{ workspace }}/seccomp.json" \
>"$tmpfile"
mv "$tmpfile" "{{ workspace }}/seccomp.json"
rm -f "$tmpfile"
_run image app_workspace with_gpu *args:
#!/usr/bin/env bash
set -eux
just ensure-network
just ensure-dns
just load-firewall-rules
just _seccomp-render
just _info "Running container for image {{ image }}..."
just _log "Workspace: {{ app_workspace }}"
extra_args=""
if [ "{{ with_gpu }}" = "true" ]; then
extra_args="$extra_args --gpus all"
else
extra_args="$extra_args
--uidmap=8000:$(id -u):1 \
--gidmap=8000:$(id -g):1 \
--uidmap=0:30000:8000 \
--gidmap=0:30000:8000"
fi
sudo podman run --rm --interactive --tty \
--read-only \
--tmpfs '/grate:noexec,nosuid,nodev,mode=1777' \
--volume '{{ app_workspace }}:/workspace:z' \
--network "{{ network }}" \
--dns "$(cat {{ workspace }}/dns)" \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--security-opt seccomp="{{ workspace }}/seccomp.json" \
--cpus {{ cpus }} \
--memory {{ memory }} \
--pids-limit {{ pids_limit }} \
$extra_args \
{{ image }} {{ args }} || true
just unload-firewall-rules
just stop-dns
just remove-network
# Open a shell to an ephemeral base image container
[group('run')]
base: check
sudo podman image exists grate-base:latest || just _build-base
@just _run grate-base:latest "{{ workspace }}/base" "false"
# Run radicle command (rad, radicle-node, etc.) inside the grate container
[group('run')]
radicle cmd *args: check
sudo podman image exists grate-radicle:latest || just _build-radicle
@just _run grate-radicle:latest "{{ workspace }}/radicle" "false" {{ cmd }} {{ args }}
start-llama-server:
@just ensure-network
@just ensure-dns
@just _info "Starting llama.cpp server..."
@just _log "Models path: {{ workspace }}/llama/models"
sudo podman image exists grate-llama-server:latest || just _build-llama-server
sudo podman run --rm --detach \
--name "grate-llama-server" \
--network "{{ network }}" \
--ip "$(awk '/\<grate-llama-server\>/ { print $1 }' '{{ workspace }}/hosts')" \
--dns "$(cat {{ workspace }}/dns)" \
--publish 8080:8080 \
--publish 8443:8443 \
--volume '{{ workspace }}/llama/models:/models:z,U' \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--cpus {{ cpus }} \
--memory {{ memory }} \
--pids-limit {{ pids_limit }} \
--gpus all \
grate-llama-server:latest
stop-llama-server:
@just _info "Stopping llama.cpp server..."
sudo podman stop grate-llama-server || true
# Run opencode inside the grate container
[group('run')]
[arg('app_workspace', long='workspace', short='w', help='mount specified directory under /workspace')]
opencode $app_workspace="" *args:
#!/usr/bin/env bash
set -eux
just start-llama-server
just _info "Starting opencode..."
sudo podman image exists grate-opencode:latest || just _build-opencode
if [ -z "$app_workspace" ]; then
app_workspace="{{ workspace }}/opencode"
fi
just _run grate-opencode:latest "$app_workspace" "false" {{ args }}
# ================
# Debug
# ================
# Build a basic debugging image with utilities
[group('debug')]
build-debug:
#!/usr/bin/env bash
just _info "Building debug image..."
ctr=$(sudo buildah from cgr.dev/chainguard/wolfi-base)
sudo buildah run "$ctr" -- apk add --no-cache kitty-terminfo iproute2 bind-tools curl nmap
sudo buildah commit "$ctr" grate-debug:latest
sudo buildah rm "$ctr"
# Run the debugging image within the network environment but no other limitations
[group('debug')]
debug-network:
#!/usr/bin/env bash
just _info "Starting network debug container..."
sudo podman image exists grate-debug:latest || just build-debug
just ensure-network
just ensure-dns
just load-firewall-rules
just _log "Network: {{ network }}"
just _log "DNS server: %s" $(cat "{{ workspace }}/dns")
sudo podman run --rm --interactive --tty \
--cap-add=ALL \
--network "{{ network }}" \
--dns "$(cat {{ workspace }}/dns)" \
grate-debug:latest || true
just unload-firewall-rules
just stop-dns
just remove-network
# Use dive to inspect an image
[group('debug')]
dive *args="--help":
sudo podman run --rm --interactive --tty \
--security-opt=label=disable \
--volume /run/user/$(id -u root)/podman/podman.sock:/var/run/docker.sock \
docker.io/wagoodman/dive:latest {{ args }}
# ================
# Security
# ================
# Print /proc/<pid>/uid_map for existing grate containers
[group('security')]
show-container-uid-maps:
#!/usr/bin/env bash
ct_names=$(sudo podman ps | awk '/grate-.*:latest/ { print $NF }')
if [ -z "$ct_names" ]; then
just _warn "No containers are running"
exit 0
fi
for ct_name in $ct_names; do
just _info "Container UID map: %s" "$ct_name"
cat /proc/$(sudo podman inspect --format '{{{{.State.Pid}}' $ct_name)/uid_map
done
_grype image:
@just _info "Scanning {{ image }} image for CVEs..."
sudo podman run --rm --tty \
--security-opt=label=disable \
--env GRYPE_DB_CACHE_DIR=/root/.cache/grype \
--volume grype-cache:/root/.cache/grype \
--volume /run/user/$(id -u root)/podman/podman.sock:/var/run/docker.sock \
docker.io/anchore/grype --ignore-states=wont-fix {{ image }}
_grype-json image:
#!/usr/bin/env bash
just _info "Scanning {{ image }} image for CVEs..."
output="$(just _sanitize-filename {{ image }} .grype.json)"
sudo podman run --rm \
--security-opt=label=disable \
--env GRYPE_DB_CACHE_DIR=/root/.cache/grype \
--volume grype-cache:/root/.cache/grype \
--volume /run/user/$(id -u root)/podman/podman.sock:/var/run/docker.sock \
--volume "{{ workspace }}:/workspace:z" \
docker.io/anchore/grype -o json="/workspace/$output" {{ image }}
sudo chown $(id -u):$(id -g) "{{ workspace }}/$output"
just _fmt-json "{{ workspace }}/$output"
_syft-json image:
#!/usr/bin/env bash
just _info "Extracting SBOM from {{ image }} image..."
output="$(just _sanitize-filename {{ image }} .syft.json)"
sudo podman run --rm \
--security-opt=label=disable \
--volume /run/user/$(id -u root)/podman/podman.sock:/var/run/docker.sock \
--volume "{{ workspace }}:/workspace:z" \
docker.io/anchore/syft scan -o json="/workspace/$output" {{ image }}
sudo chown $(id -u):$(id -g) "{{ workspace }}/$output"
just _fmt-json "{{ workspace }}/$output"
_update-cisa-kev-catalog:
#!/usr/bin/env bash
just _info "Updating CISA KEV catalog..."
now=$(date +%s)
mtime=$(stat -c %Y "{{ workspace }}/cisa-kev-catalog.json")
age=$((now - mtime))
if [ "$age" -ge 21600 ]; then
just _info "Downloading latest version of CISA KEV catalog..."
curl -s "{{ cisa_kev_catalog_url }}" -o "{{ workspace }}/cisa-kev-catalog.json"
else
just _warn "CISA KEV catalog is less than 6h old, not downloading"
fi
_grype-vulnerability-info image id:
#!/usr/bin/env bash
grype_report="$(just _sanitize-filename {{ image }} .grype.json)"
artifact=$(jq 'first(.matches[] | select(.vulnerability.id == "{{ id }}")) | .artifact' \
"{{ workspace }}/$grype_report")
fix=$(jq 'first(.matches[].vulnerability | select(.id == "{{ id }}")) | .fix' \
"{{ workspace }}/$grype_report")
severity=$(jq -r 'first(.matches[].vulnerability | select(.id == "{{ id }}")) | .severity' \
"{{ workspace }}/$grype_report")
epss=$(jq -r 'first(.matches[].vulnerability | select(.id == "{{ id }}")) | .epss[0].epss' \
"{{ workspace }}/$grype_report")
cvss=$(jq -r 'first(.matches[].vulnerability | select(.id == "{{ id }}")) | .cvss[0].metrics.baseScore' \
"{{ workspace }}/$grype_report")
risk=$(jq -r 'first(.matches[].vulnerability | select(.id == "{{ id }}")) | .risk' \
"{{ workspace }}/$grype_report")
printf "\tName\t\t%s\n" $(jq -r '.name' <<<"$artifact")
printf "\tVersion\t\t%s\n" $(jq -r '.version' <<<"$artifact")
if [ "$(jq -r '.state' <<<$fix)" = "fixed" ]; then
printf "\tFixed\t\t%s\n" $(jq -r '.available[0].version' <<<"$fix")
fi
printf "\tType\t\t%s\n" $(jq -r '.type' <<<"$artifact")
printf "\tSeverity\t%s\n" "$severity"
printf "\tEPSS\t\t%s\n" "$epss"
printf "\tCVSS\t\t%s\n" "$cvss"
printf "\tRisk\t\t%s\n" "$risk"
_cross-reference-grype-and-cisa-kev-catalog image:
#!/usr/bin/env bash
grype_report="$(just _sanitize-filename {{ image }} .grype.json)"
comm -12 \
<(jq -r '.matches[].vulnerability.id' "{{ workspace }}/$grype_report" | sort -u) \
<(jq -r '.vulnerabilities[].cveID' "{{ workspace }}/cisa-kev-catalog.json" | sort -u) | \
while read -r cve; do
just _panic "CISA KEV catalog CVE found: $cve"
just _grype-vulnerability-info "{{ image }}" "$cve"
done
_filter-epss image:
#!/usr/bin/env bash
grype_report="$(just _sanitize-filename {{ image }} .grype.json)"
jq -r '.matches[].vulnerability | select(.epss[]?.epss > 0.7) | .id' \
"{{ workspace }}/$grype_report" | sort -u | \
while read -r cve; do
just _critical "High EPSS CVE found: $cve"
just _grype-vulnerability-info "{{ image }}" "$cve"
done
_filter-cvss image:
#!/usr/bin/env bash
grype_report="$(just _sanitize-filename {{ image }} .grype.json)"
jq -r '.matches[].vulnerability | select(.cvss[]?.metrics.baseScore > 9) | .id' \
"{{ workspace }}/$grype_report" | sort -u | \
while read -r cve; do
just _warn "High CVSS CVE found: $cve"
just _grype-vulnerability-info "{{ image }}" "$cve"
done
# Scan image for CVEs with grype and apply vulnerability management policy
[group('security')]
[arg('mode', long, short='m', help="'raw' for a full scan, 'policy' to filter high priority")]
scan-image image mode='policy':
#!/usr/bin/env bash
just _info "Scanning {{ image }} image in {{ mode }} mode..."
mode="{{ mode }}"
case $mode in
raw)
just _grype {{ image }}
;;
policy)
just _grype-json {{ image }}
just _update-cisa-kev-catalog
just _info "Filtering based on risk using vulnerability management policy..."
just _cross-reference-grype-and-cisa-kev-catalog {{ image }}
just _filter-epss {{ image }}
just _filter-cvss {{ image }}
just _info "If you see nothing above, that's good (or retry with '--mode raw' for a full CVE audit)."
;;
*)
just _error "unsupported mode: $mode (expected: raw, policy)"
esac
# Scan all grate images for CVEs with grype and apply vulnerability management policy
[group('security')]
[arg('mode', long, short='m', help="'raw' for a full scan, 'policy' to filter high priority")]
scan mode='policy':
just _info "Scanning all grate-* images in {{ mode }} mode..."
sudo podman images --format '{{{{.Repository}}' "grate-*" | xargs -I"{}" just scan-image "{}" --mode "{{ mode }}"