carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# Plan-time tests. mock_provider: no account, no credentials.
2mock_provider "aws" {}
3
4override_data {
5 target = data.aws_security_group.client
6 values = { vpc_id = "vpc-0aaaaaaaaaaaaaaaa" }
7}
8
9override_data {
10 target = data.aws_vpc.client
11 values = { default = true }
12}
13
14variables {
15 name = "carlos-test-scanner"
16 ami_id = "ami-0123456789abcdef0"
17 client_security_group_id = "sg-0123456789abcdef0"
18 region = "eu-west-1"
19}
20
21run "the_fence" {
22 command = plan
23 assert {
24 condition = length(aws_security_group.scanner.ingress) == 1
25 error_message = "exactly one way in"
26 }
27 assert {
28 condition = alltrue([
29 for i in aws_security_group.scanner.ingress :
30 i.from_port == 3310 && i.to_port == 3310 && i.protocol == "tcp"
31 && i.security_groups == toset(["sg-0123456789abcdef0"])
32 && try(length(i.cidr_blocks), 0) == 0 && try(length(i.ipv6_cidr_blocks), 0) == 0
33 ])
34 error_message = "3310 from the client security group only, never a CIDR"
35 }
36 assert {
37 condition = aws_security_group.scanner.vpc_id == "vpc-0aaaaaaaaaaaaaaaa"
38 error_message = "the group lands in the client group's VPC"
39 }
40}
41
42run "the_box" {
43 command = plan
44 assert {
45 condition = aws_instance.scanner.metadata_options[0].http_tokens == "required"
46 error_message = "IMDSv2 only"
47 }
48 assert {
49 condition = aws_instance.scanner.root_block_device[0].encrypted
50 error_message = "encrypted root volume"
51 }
52 assert {
53 condition = aws_instance.scanner.instance_type == "t4g.medium"
54 error_message = "t4g.medium by default"
55 }
56 assert {
57 # base64 is 4 characters per 3 bytes, so 21800 characters is under 16 KiB.
58 condition = length(base64gzip(local.init_script)) < 21800
59 error_message = "EC2 refuses user_data over 16 KiB, and the script is sent gzipped to stay well under it"
60 }
61}
62
63run "the_role_cannot_read_parameters" {
64 command = plan
65 assert {
66 condition = jsondecode(aws_iam_role_policy.no_parameters.policy).Statement[0].Effect == "Deny"
67 error_message = "an explicit Deny, because the managed SSM policy allows GetParameter* on *"
68 }
69 assert {
70 condition = toset(jsondecode(aws_iam_role_policy.no_parameters.policy).Statement[0].Action) == toset([
71 "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParameterHistory", "ssm:GetParametersByPath",
72 ])
73 error_message = "all four parameter-read actions"
74 }
75 assert {
76 condition = jsondecode(aws_iam_role_policy.no_parameters.policy).Statement[0].Resource == "*"
77 error_message = "on every parameter: this box is allowed none"
78 }
79}
80
81run "the_engine_config" {
82 command = plan
83 assert {
84 condition = strcontains(local.init_script, "setkey MaxFileSize 100M") && strcontains(local.init_script, "setkey MaxScanSize 100M")
85 error_message = "both limits 100M: equal is what makes a trimmed member trip the scan budget"
86 }
87 assert {
88 condition = strcontains(local.init_script, "setkey AlertExceedsMax yes")
89 error_message = "limits must alert"
90 }
91 assert {
92 condition = strcontains(local.init_script, "setkey StreamMaxLength 25M")
93 error_message = "StreamMaxLength is stream_max_bytes, written in M so a debconf rewrite cannot turn it into 25 TiB"
94 }
95 assert {
96 condition = strcontains(local.init_script, "echo 'clamav-daemon clamav-daemon/debconf boolean false' | debconf-set-selections")
97 error_message = "package upgrades never regenerate clamd.conf"
98 }
99 assert {
100 condition = alltrue([
101 strcontains(local.init_script, "cat > /etc/apparmor.d/local/usr.sbin.clamd <<'EOF'"),
102 strcontains(local.init_script, "\n/var/tmp/ rw,\n/var/tmp/** krw,\n"),
103 strcontains(local.init_script, "apparmor_parser -r /etc/apparmor.d/usr.sbin.clamd"),
104 strcontains(local.init_script, "setkey TemporaryDirectory /var/tmp"),
105 ])
106 error_message = "clamd's AppArmor profile allows its TemporaryDirectory, /var/tmp, or every scan errors"
107 }
108 assert {
109 condition = strcontains(local.init_script, "sed -i '/^TCPSocket[[:space:]]/d;/^TCPAddr[[:space:]]/d' \"$conf\"")
110 error_message = "any TCPSocket or TCPAddr line the package wrote is deleted"
111 }
112 assert {
113 condition = !strcontains(local.init_script, "setkey TCPSocket") && !strcontains(local.init_script, "setkey TCPAddr")
114 error_message = "clamd itself never listens on TCP; the gated proxy does"
115 }
116 assert {
117 condition = output.stream_max_bytes == 26214400
118 error_message = "the console's CARLOS_CLAMD_MAX_BYTES comes from this output"
119 }
120}
121
122run "small_box_with_concurrent_reload_is_refused" {
123 command = plan
124 variables {
125 instance_type = "t4g.small"
126 }
127 expect_failures = [aws_instance.scanner]
128}
129
130run "small_box_without_concurrent_reload_is_fine" {
131 command = plan
132 variables {
133 instance_type = "t4g.small"
134 concurrent_database_reload = false
135 }
136 assert {
137 condition = strcontains(local.init_script, "setkey ConcurrentDatabaseReload no")
138 error_message = "the reload setting reaches clamd.conf"
139 }
140}
141
142run "stream_limit_too_small_for_the_fixtures" {
143 command = plan
144 variables {
145 stream_max_bytes = 1048576
146 }
147 expect_failures = [var.stream_max_bytes]
148}
149
150run "stream_limit_above_the_scan_budget" {
151 command = plan
152 variables {
153 stream_max_bytes = 209715200
154 }
155 expect_failures = [var.stream_max_bytes]
156}
157
158run "stream_limit_not_a_whole_mib" {
159 command = plan
160 variables {
161 stream_max_bytes = 26214401
162 }
163 expect_failures = [var.stream_max_bytes]
164}
165
166run "the_units" {
167 command = plan
168 assert {
169 condition = alltrue([
170 for line in [
171 "DefaultDependencies=no",
172 "Requires=scanner-selftest.service",
173 "BindsTo=scanner-selftest.service",
174 "After=scanner-selftest.service",
175 "ListenStream=@PRIVATE_IP@:3310",
176 "FreeBind=yes",
177 ] : strcontains(file("${path.module}/files/scanner-tcp.socket"), "\n${line}\n")
178 ])
179 error_message = "the socket opts out of default dependencies, is bound to the self-test and listens on the private address only"
180 }
181 assert {
182 condition = alltrue([
183 for line in [
184 "Requires=scanner-selftest.service",
185 "BindsTo=scanner-selftest.service",
186 "After=scanner-selftest.service",
187 ] : strcontains(file("${path.module}/files/scanner-tcp.service"), "\n${line}\n")
188 ])
189 error_message = "the proxy is bound to the self-test, so connections in flight are cut with it"
190 }
191 assert {
192 condition = alltrue([
193 for line in [
194 "BindsTo=clamav-daemon.service",
195 "After=clamav-daemon.service",
196 "Wants=scanner-tcp.socket",
197 "Type=oneshot",
198 "RemainAfterExit=yes",
199 "PrivateTmp=yes",
200 ] : strcontains(file("${path.module}/files/scanner-selftest.service"), "\n${line}\n")
201 ])
202 error_message = "the self-test follows clamd, asks for the gate, stays active after a pass, and keeps its fixtures in a private /var/tmp"
203 }
204 assert {
205 # Comments may sit between the heredoc line and [Unit]; the Wants= line
206 # must be the first line of the [Unit] section.
207 condition = can(regex("10-scanner\\.conf <<EOF\\n(#[^\\n]*\\n)*\\[Unit\\]\\nWants=scanner-tcp\\.socket\\n", local.init_script))
208 error_message = "every start of clamd, an automatic restart included, asks for the gate again"
209 }
210}
211
212run "the_selftest_classifier" {
213 command = plan
214 assert {
215 condition = !strcontains(file("${path.module}/files/scanner-selftest"), "*\"Heuristics.Limits.Exceeded\"*")
216 error_message = "no substring glob: it accepted MaxScanTime and lookalikes such as Win.Heuristics.Limits.Exceeded.Fake as a limit"
217 }
218 assert {
219 condition = strcontains(file("${path.module}/files/scanner-selftest"), "Heuristics.Limits.Exceeded.MaxScanTime) echo timeout ;;")
220 error_message = "a scan timeout is its own class, which no fixture wants"
221 }
222 assert {
223 condition = strcontains(file("${path.module}/files/scanner-selftest"), "Heuristics.Limits.Exceeded | Heuristics.Limits.Exceeded.*) echo limit ;;")
224 error_message = "a limit is the heuristic itself or a name under it, dot included, as internal/clamd reads it"
225 }
226}
227
228run "logging_cannot_kill_init" {
229 command = plan
230 assert {
231 condition = !strcontains(local.init_script, "tee /dev/console")
232 error_message = "a failed console write would exit under set -e with no FAILED marker"
233 }
234 assert {
235 condition = strcontains(local.init_script, "{ echo \"scanner-init: $*\" > /dev/console; } 2>/dev/null || true")
236 error_message = "the console copy of each line is best effort"
237 }
238}
239
240run "freshclam_uses_its_packaged_config" {
241 command = plan
242 assert {
243 condition = !strcontains(local.init_script, "freshclam --config-file") && !strcontains(local.init_script, "/run/freshclam")
244 error_message = "Debian's AppArmor profile confines freshclam to its config under /etc/clamav: a config passed from anywhere else cannot be read and freshclam fails at once"
245 }
246 assert {
247 condition = strcontains(local.init_script, "until freshclam; do")
248 error_message = "the first signatures must come from plain freshclam with the packaged config"
249 }
250}
251
252run "another_vpc_needs_a_subnet" {
253 command = plan
254 override_data {
255 target = data.aws_vpc.client
256 values = { default = false }
257 }
258 expect_failures = [aws_instance.scanner]
259}
260