forked from kubernetes/k8s.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathaudit-gcp.sh
executable file
·690 lines (633 loc) · 24.6 KB
/
audit-gcp.sh
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
680
681
682
683
684
685
686
687
688
689
690
#!/usr/bin/env bash
# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Exports select GCP resources in the kubernetes.io organization managed
# by sig-k8s-infra, for purposes of auditing review.
#
# Must be run by an authenticated member of the GCP auditors group
# ([email protected]) or as a service-account that has the
# custom organization IAM role "audit.viewer" assigned at the organization level
#
# Usage:
#
# # remove/re-export all resources
# audit-gcp.sh
#
# # remove/re-export all resources in projects foo, bar
# audit-gcp.sh foo bar
#
# # remove/re-export (iam, gcs, monitoring) resources in project foo
# K8S_INFRA_AUDIT_SERVICES="storage-api,monitoring" audit-gcp.sh foo
set -o errexit
set -o nounset
set -o pipefail
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
readonly REPO_ROOT
. "${REPO_ROOT}/infra/gcp/bash/lib.sh"
#
# config
#
# where audit files will be exported
readonly AUDIT_DIR="${REPO_ROOT}/audit"
# hardcoded id for organization: kubernetes.io
readonly KUBERNETES_IO_GCP_ORG="758905017065"
# which services to try exporting; default empty, which means all
# e.g. K8S_INFRA_AUDIT_SERVICES="storage-api,monitoring" audit-gcp.sh k8s-infra-foo
AUDIT_SERVICES=()
IFS=', ' read -r -a AUDIT_SERVICES <<< "${K8S_INFRA_AUDIT_SERVICES:-""}"
readonly AUDIT_SERVICES
#
# utils
#
function ensure_audit_dependencies() {
echo "bq"
# the 'bq show' command is called as a hack to dodge the config prompts that bq presents
# the first time it is run. A newline is passed to stdin to skip the prompt for default project
# when the service account in use has access to multiple projects.
if ! bq show <<< $'\n' >/dev/null; then
# ignore errors from bq while doing this hack
true
fi
echo "gcloud config"
gcloud config list
# right now most of this script assumes it's been run within the audit dir
pushd "${AUDIT_DIR}" >/dev/null
}
function format_gcloud_json() {
# recursively delete any fields named "etag"
jq 'delpaths([path(..|.etag?|select(.))])'
}
function ensure_clean_dir() {
rm -rf "${1}" && mkdir -p "${1}"
}
#
# main
#
function remove_all_gcp_project_audit_files() {
rm -rf projects
}
function audit_gcp_organization() {
local org_name="${1}"
local org_id
local org_dir="organizations/${org_name}"
echo "Removing existing audit files for organization: ${org_name}"
ensure_clean_dir "${org_dir}"
echo "Exporting organization description for organization: ${org_name}"
gcloud \
organizations describe "${org_name}" \
--format=json | format_gcloud_json \
> "${org_dir}/description.json"
# gcloud iam calls require the numeric organization id
org_id=$(<"${org_dir}/description.json" jq -r .name | cut -d/ -f2)
echo "Exporting IAM policy for organization: ${org_name}"
gcloud \
organizations get-iam-policy "${org_id}" \
--format=json | format_gcloud_json \
> "${org_dir}/iam.json"
echo "Exporting IAM roles for organization: ${org_name}"
ensure_clean_dir "${org_dir}/roles"
mapfile -t roles < <(
gcloud iam roles list --organization="${org_id}" --format="value(name)"
)
for role in "${roles[@]##*/}"; do
echo "role: ${role}"
gcloud \
iam roles describe "${role}" \
--organization="${org_id}" \
--format=json | format_gcloud_json \
> "${org_dir}/roles/${role}.json"
done 2>&1 | indent
}
function audit_all_projects_with_parent_id() {
local parent_id="${1}"
echo "Removing existing audit files"
ensure_clean_dir "projects/"
gcloud \
projects list \
--filter="parent.id=${parent_id}" \
--format="value(name)" \
| sort \
| while read -r project; do
echo "Exporting GCP project: ${project}"
audit_gcp_project "${project}" 2>&1 | indent
done
}
function audit_gcp_project() {
local project="${1}"
local project_dir="projects/${project}"
echo "Removing existing audit files for project: ${project}"
ensure_clean_dir "${project_dir}"
echo "Exporting project description for project: ${project}"
gcloud \
projects describe "${project}" \
--format=json | format_gcloud_json \
> "${project_dir}/description.json"
echo "Exporting IAM policy for project: ${project}"
gcloud \
projects get-iam-policy "${project}" \
--format=json | format_gcloud_json \
> "${project_dir}/iam.json"
echo "Exporting IAM serviceaccounts for project: ${project}"
ensure_clean_dir "${project_dir}/service-accounts"
gcloud \
iam service-accounts list \
--project="${project}" \
--format="value(email)" \
| while read -r SVCACCT; do
echo "serviceaccount: ${SVCACCT}"
mkdir -p "${project_dir}/service-accounts/${SVCACCT}"
gcloud \
iam service-accounts describe "${SVCACCT}" \
--project="${project}" \
--format=json | format_gcloud_json \
> "${project_dir}/service-accounts/${SVCACCT}/description.json"
gcloud \
iam service-accounts get-iam-policy "${SVCACCT}" \
--project="${project}" \
--format=json | format_gcloud_json \
> "${project_dir}/service-accounts/${SVCACCT}/iam.json"
done
echo "Exporting IAM roles for project: ${project}"
ensure_clean_dir "projects/${project}/roles"
gcloud \
iam roles list \
--project="${project}" \
--format="value(name)" \
| while read -r role_path; do
role=$(basename "${role_path}")
gcloud \
iam roles describe "${role}" \
--project="${project}" \
--format=json | format_gcloud_json \
> "${project_dir}/roles/${role}.json"
done
audit_gcp_project_services "${project}" "${AUDIT_SERVICES[@]}"
}
function audit_gcp_project_services() {
local project="${1}"; shift
local services=("$@")
local which_services="manually specified"
local services_dir="projects/${project}/services"
echo "Exporting enabled services for project: ${project}"
mkdir -p "${services_dir}"
gcloud \
services list \
--project="${project}" \
--filter="state:ENABLED" \
> "${services_dir}/enabled.txt"
if [ ${#services[@]} -eq 0 ]; then
which_services="all enabled"
mapfile -t services < <(<"${services_dir}/enabled.txt" tail +2 | cut -d' ' -f1)
find "${services_dir}" -mindepth 1 -maxdepth 1 -type d | xargs --null rm -rf
fi
echo "Exporting resources for ${which_services} services for project: ${project}"
for service in "${services[@]}"; do
service="${service%.googleapis.com}"
audit_gcp_project_service "${project}" "${service}"
done 2>&1 | indent
}
function audit_gcp_project_service() {
local project="${1}"
local service="${2}"
local service_dir="projects/${project}/services/${service}"
local skip=true
ensure_clean_dir "${service_dir}"
case "${service}" in
bigquerystorage)
echo "Skipping service: ${service}, no resources to export"
;;
cloudshell)
echo "Skipping service: ${service}, no resources to export"
;;
iam)
# TODO: ideally we would export iam resources here instead of elsewhere
echo "Skipping service: ${service}, handled outside of service export"
# TODO: gcloud iam workload-identity-pools
;;
iamcredentials)
echo "Skipping service: ${service}, no resources to export"
;;
oslogin)
echo "Skipping service: ${service}, no resources to export"
;;
serviceusage)
echo "Skipping service: ${service}, no resources to export"
;;
storage-component)
echo "Skipping service: ${service}, same resources as handled service: storage-api"
;;
*)
echo "Exporting resources for service: ${service}, project: ${project}"
skip=false
;;
esac
if "${skip}"; then
return
fi
case "${service}" in
bigquery)
echo "datasets"
bq \
ls \
--project_id="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/bigquery.datasets.json"
# Only run if there are any datasets
if [ -s "${service_dir}/bigquery.datasets.json" ]
then
bq \
ls \
--project_id="${project}" \
--format=json | format_gcloud_json \
| jq -r '.[] | .datasetReference["datasetId"]' \
| while read -r DATASET; do
echo "dataset access: ${DATASET}"
bq \
show \
--project_id="${project}" \
--format=json \
"${project}:${DATASET}" \
| format_gcloud_json \
| jq .access \
> "${service_dir}/bigquery.datasets.${DATASET}.access.json"
done
fi
;;
cloudasset)
echo "feeds"
gcloud \
asset feeds list \
--project="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/feeds.json"
if [ "$(cat "${service_dir}/feeds.json")" == "{}" ]; then
rm "${service_dir}/feeds.json"
fi
;;
cloudfunctions)
echo "functions"
gcloud \
functions list \
--project="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/functions.json"
if [ "$(cat "${service_dir}/functions.json")" == "[]" ]; then
rm "${service_dir}/functions.json"
fi
;;
cloudresourcemanager)
# TODO: this service should maybe be ignored and done as a special-case at the org level
echo "org-policies"
gcloud \
resource-manager org-policies list \
--project="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/org-policies.json"
if [ "$(cat "${service_dir}/org-policies.json")" == "[]" ]; then
rm "${service_dir}/org-policies.json"
fi
# TODO: gcloud alpha resource-manager liens ?
# TODO: gcloud alpha resource-manager tags ?
;;
compute)
echo "project-info"
gcloud \
compute project-info describe \
--project="${project}" \
--format=json | format_gcloud_json \
| jq 'del(.quotas[].usage, .commonInstanceMetadata.fingerprint)' \
> "${service_dir}/project-info.json"
# TODO: gcloud compute * ?
;;
container)
echo "clusters"
# TODO: this may get noisy since there are things that change
# without human interaction; prune more fields as discovered
local clusters_dir="${service_dir}/clusters"
ensure_clean_dir "${clusters_dir}"
gcloud \
container clusters list \
--project="${project}" \
--format=json | format_gcloud_json \
| jq -r 'map("\(.name) \(.)")[]' \
| while read -r name json; do \
echo "cluster: ${name}"
echo "${json}" \
| jq 'del(.masterAuth, .status, .nodePools[].status, .currentNodeCount)' \
> "${clusters_dir}/${name}.json"
done
# TODO: gcloud container binauthz ?
# TODO: gcloud container node-pools ?
# TODO: gcloud container subnets ?
;;
datastore)
echo "TODO: insufficient permissions"
# TODO: gcloud datatore indexes list # ERROR: (gcloud.datastore.indexes.list) caller does not have permission
;;
dns)
echo "project-info"
gcloud \
dns project-info describe "${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/info.json"
echo "managed-zones"
gcloud \
dns managed-zones list \
--project="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/zones.json"
# TODO: gcloud dns dns-keys ?
# TODO: gcloud dns polocies ?
# TODO: gcloud dns record-sets ?
;;
logging)
# TODO: does this service actually need serviceusage.services.use?
echo "logs"
if [[ "${project}" =~ ^k8s-infra-e2e-.* ]]; then
echo "skipping for ${project}; logs from e2e test pods are causing noisy churn"
else
gcloud \
logging logs list \
--project="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/logs.json"
fi
echo "metrics"
gcloud \
logging metrics list \
--project="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/metrics.json"
if [ "$(cat "${service_dir}/metrics.json")" == "[]" ]; then
rm "${service_dir}/metrics.json"
fi
echo "sinks"
gcloud \
logging sinks list \
--project="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/sinks.json"
# TODO: gcloud logging buckets ?
# TODO: gcloud logging views ?
;;
monitoring)
# TODO: does this service actually need serviceusage.services.use?
echo "dashboards"
local dashboards_dir="${service_dir}/dashboards"
ensure_clean_dir "${dashboards_dir}"
gcloud \
monitoring dashboards list \
--project="${project}" \
--format=json | format_gcloud_json \
| jq -r 'map("\(.displayName) \(.)")[]' \
| while read -r name json; do \
echo "dashboard: ${name}"
echo "${json}" \
> "${dashboards_dir}/${name}.json"
done
# TODO: gcloud beta monitoring channel-descriptors
# TODO: gcloud beta monitoring channels list
# TODO gcloud alpha monitoring policies list
;;
pubsub)
echo "snapshots"
gcloud \
pubsub snapshots list \
--project="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/snapshots.json"
if [ "$(cat "${service_dir}/snapshots.json")" == "[]" ]; then
rm "${service_dir}/snapshots.json"
fi
echo "subscriptions"
gcloud \
pubsub subscriptions list \
--project="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/subscriptions.json"
if [ "$(cat "${service_dir}/subscriptions.json")" == "[]" ]; then
rm "${service_dir}/subscriptions.json"
fi
echo "topics"
gcloud \
pubsub topics list \
--project="${project}" \
--format=json | format_gcloud_json \
> "${service_dir}/topics.json"
if [ "$(cat "${service_dir}/topics.json")" == "[]" ]; then
rm "${service_dir}/topics.json"
fi
# TODO: gcloud pubsub lite-subscriptions ?
# TODO: gcloud pubsub lite-topics ?
;;
secretmanager)
gcloud \
secrets list \
--project="${project}" \
--format="value(name)" \
| while read -r SECRET; do
echo "secret: ${SECRET}"
path="projects/${project}/secrets/${SECRET}"
mkdir -p "${path}"
gcloud \
secrets describe "${SECRET}" \
--project="${project}" \
--format=json | format_gcloud_json \
> "${path}/description.json"
gcloud \
secrets versions list "${SECRET}" \
--project="${project}" \
--format=json \
> "${path}/versions.json"
gcloud \
secrets get-iam-policy "${SECRET}" \
--project="${project}" \
--format=json | format_gcloud_json \
> "${path}/iam.json"
done
;;
storage-api)
local buckets_dir="projects/${project}/buckets"
ensure_clean_dir "${buckets_dir}"
# save gsutil calls by listing all buckets at once, then splitting
# into separate files with awk after the fact
gsutil ls -L -b -p "${project}" \
| awk "/^gs:/ { split(\$1,a,\"/\"); f=\"${buckets_dir}/\" a[3] \".txt\"} {print > f}"
mapfile -t buckets < <(
find "${buckets_dir}" -maxdepth 1 -name '*.txt' -exec basename {} .txt \;
)
for bucket in "${buckets[@]}"; do
echo "bucket: ${bucket}"
local bucket_dir="${buckets_dir}/${bucket}"
ensure_clean_dir "${bucket_dir}"
mv "${buckets_dir}/${bucket}"{,/metadata}.txt
done
# save gsutil calls for per-bucket configuration by only getting
# configuration that is Present according to bucket metadata
for bucket in "${buckets[@]}"; do
local bucket_dir="${buckets_dir}/${bucket}"
echo "bucket iam: ${bucket}"
gsutil iam get "gs://${bucket}" | format_gcloud_json \
> "${bucket_dir}/iam.json"
if grep -q "Logging configuration:.*Present" "${bucket_dir}/metadata.txt"; then
echo "bucket logging: ${bucket}"
gsutil logging get "gs://${bucket}" \
> "${bucket_dir}/logging.json"
fi
if grep -q "Retention Policy:.*Present" "${bucket_dir}/metadata.txt"; then
echo "bucket retention: ${bucket}"
gsutil retention get "gs://${bucket}" \
> "${bucket_dir}/retention.txt"
fi
if grep -q "Lifecycle configuration:.*Present" "${bucket_dir}/metadata.txt"; then
echo "bucket lifecycle: ${bucket}"
gsutil lifecycle get "gs://${bucket}" \
> "${bucket_dir}/lifecycle.json"
fi
done
;;
*)
echo "WARN: Unaudited service ${service} enabled in project: ${project}"
# TODO: handle or ignore: NAME
# TODO: handle or ignore: admin
# TODO: handle or ignore: bigqueryconnection
# TODO: handle or ignore: bigquerydatatransfer
# TODO: handle or ignore: bigqueryreservation
# TODO: handle or ignore: cloudapis
# TODO: handle or ignore: cloudbuild
# TODO: handle or ignore: clouddebugger
# TODO: handle or ignore: clouderrorreporting
# TODO: handle or ignore: cloudkms
# TODO: handle or ignore: cloudtrace
# TODO: handle or ignore: containeranalysis
# TODO: handle or ignore: containerregistry
# TODO: handle or ignore: containerscanning
# TODO: handle or ignore: deploymentmanager
# TODO: handle or ignore: groupssettings
# TODO: handle or ignore: run
# TODO: handle or ignore: servicemanagement
# TODO: handle or ignore: serviceusage
# TODO: handle or ignore: source
# TODO: handle or ignore: sql-component
# TODO: handle or ignore: stackdriver
;;
esac 2>&1 | indent
}
function audit_k8s_infra_gcp() {
echo "Removing all existing GCP project audit files"
remove_all_gcp_project_audit_files 2>&1 | indent
echo "Exporting GCP organization: kubernetes.io"
audit_gcp_organization "kubernetes.io" 2>&1 | indent
# TODO: this will miss projects that are under folders
echo "Exporting all GCP projects with parent id: ${KUBERNETES_IO_GCP_ORG}"
audit_all_projects_with_parent_id "${KUBERNETES_IO_GCP_ORG}" 2>&1 | indent
echo "Done"
}
function migrate_audit_format() {
local migrated=false
local projects=("$@")
if [ $# -eq 0 ]; then
mapfile -t projects < <(echo projects/* | xargs basename)
fi
echo "Migrating audit format for projects: ${projects[*]}"
if [ -d org_kubernetes.io ]; then
mkdir -p organizations/kubernetes.io
git mv org_kubernetes.io/* organizations/kubernetes.io
rm -rf org_kubernetes.io
migrated=true
fi
for project in "${projects[@]}"; do
local project_dir=projects/${project}
# migrate container)
local old_clusters="${project_dir}/services/container/clusters.txt"
if [ -f "${old_clusters}" ]; then
local clusters_dir="${project_dir}/services/container/clusters"
mkdir -p "${clusters_dir}"
while read -r name zone _; do
local new_cluster="${clusters_dir}/$name.json"
echo "{ \"name\": \"$name\", \"location\": \"$zone\" }" \
| jq > "${new_cluster}"
git add "${new_cluster}"
done <"${old_clusters}"
git rm "${old_clusters}"
migrated=true
fi
# migrate storage-api)
local cors ubla logging
for bucket in $(find "${project_dir}" -name 'bucketpolicyonly.txt' | cut -d/ -f4 | sort); do
local bucket_dir=${project_dir}/buckets/${bucket}
cors="None"
if ! grep -q "has no CORS configuration" "${bucket_dir}/cors.txt"; then
# not actually sure what it would be, none of our buckets have it
cors="Present"
else
git rm "${bucket_dir}/cors.txt"
fi
logging="None"
if ! grep -q "has no logging configuration" "${bucket_dir}/logging.txt"; then
logging="Present"
git mv "${bucket_dir}/logging.txt" "${bucket_dir}/logging.json"
else
git rm "${bucket_dir}/logging.txt"
fi
ubla="False"
if grep -q "Enabled: True" "${bucket_dir}/bucketpolicyonly.txt"; then
ubla="True"
fi
git rm "${bucket_dir}/bucketpolicyonly.txt"
# there are very intentionally tab characters in this, since that's what gsutil outputs
cat >"${bucket_dir}/metadata.txt" <<EOF
gs://${bucket}/ :
Storage class: STANDARD
Location type: multi-region
Location constraint: US
Versioning enabled: None
Logging configuration: ${logging}
Website configuration: None
CORS configuration: ${cors}
Lifecycle configuration: None
Requester Pays enabled: None
Labels: None
Default KMS key: None
Time created: TBD
Time updated: TBD
Metageneration: 8
Bucket Policy Only enabled: ${ubla}
ACL: []
Default ACL: []
EOF
git add "${bucket_dir}/metadata.txt"
migrated=true
done
done
if ${migrated}; then
git commit -m "audit: migrate to new file layout"
fi
}
function main() {
local projects=("$@")
echo "Ensuring dependencies"
ensure_audit_dependencies
migrate_audit_format "${projects[@]}"
if [ "${#projects[@]}" -eq 0 ]; then
audit_k8s_infra_gcp
else
for project in "$@"; do
echo "Exporting GCP project: ${project} services: ${AUDIT_SERVICES[*]}"
audit_gcp_project "${project}" 2>&1 | indent
done
fi
echo "Done"
}
main "$@"