# Plan-time tests. mock_provider: no account, no credentials. mock_provider "aws" {} override_data { target = data.aws_security_group.client values = { vpc_id = "vpc-0aaaaaaaaaaaaaaaa" } } override_data { target = data.aws_vpc.client values = { default = true } } variables { name = "carlos-test-scanner" ami_id = "ami-0123456789abcdef0" client_security_group_id = "sg-0123456789abcdef0" region = "eu-west-1" } run "the_fence" { command = plan assert { condition = length(aws_security_group.scanner.ingress) == 1 error_message = "exactly one way in" } assert { condition = alltrue([ for i in aws_security_group.scanner.ingress : i.from_port == 3310 && i.to_port == 3310 && i.protocol == "tcp" && i.security_groups == toset(["sg-0123456789abcdef0"]) && try(length(i.cidr_blocks), 0) == 0 && try(length(i.ipv6_cidr_blocks), 0) == 0 ]) error_message = "3310 from the client security group only, never a CIDR" } assert { condition = aws_security_group.scanner.vpc_id == "vpc-0aaaaaaaaaaaaaaaa" error_message = "the group lands in the client group's VPC" } } run "the_box" { command = plan assert { condition = aws_instance.scanner.metadata_options[0].http_tokens == "required" error_message = "IMDSv2 only" } assert { condition = aws_instance.scanner.root_block_device[0].encrypted error_message = "encrypted root volume" } assert { condition = aws_instance.scanner.instance_type == "t4g.medium" error_message = "t4g.medium by default" } assert { # base64 is 4 characters per 3 bytes, so 21800 characters is under 16 KiB. condition = length(base64gzip(local.init_script)) < 21800 error_message = "EC2 refuses user_data over 16 KiB, and the script is sent gzipped to stay well under it" } } run "the_role_cannot_read_parameters" { command = plan assert { condition = jsondecode(aws_iam_role_policy.no_parameters.policy).Statement[0].Effect == "Deny" error_message = "an explicit Deny, because the managed SSM policy allows GetParameter* on *" } assert { condition = toset(jsondecode(aws_iam_role_policy.no_parameters.policy).Statement[0].Action) == toset([ "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParameterHistory", "ssm:GetParametersByPath", ]) error_message = "all four parameter-read actions" } assert { condition = jsondecode(aws_iam_role_policy.no_parameters.policy).Statement[0].Resource == "*" error_message = "on every parameter: this box is allowed none" } } run "the_engine_config" { command = plan assert { condition = strcontains(local.init_script, "setkey MaxFileSize 100M") && strcontains(local.init_script, "setkey MaxScanSize 100M") error_message = "both limits 100M: equal is what makes a trimmed member trip the scan budget" } assert { condition = strcontains(local.init_script, "setkey AlertExceedsMax yes") error_message = "limits must alert" } assert { condition = strcontains(local.init_script, "setkey StreamMaxLength 25M") error_message = "StreamMaxLength is stream_max_bytes, written in M so a debconf rewrite cannot turn it into 25 TiB" } assert { condition = strcontains(local.init_script, "echo 'clamav-daemon clamav-daemon/debconf boolean false' | debconf-set-selections") error_message = "package upgrades never regenerate clamd.conf" } assert { condition = alltrue([ strcontains(local.init_script, "cat > /etc/apparmor.d/local/usr.sbin.clamd <<'EOF'"), strcontains(local.init_script, "\n/var/tmp/ rw,\n/var/tmp/** krw,\n"), strcontains(local.init_script, "apparmor_parser -r /etc/apparmor.d/usr.sbin.clamd"), strcontains(local.init_script, "setkey TemporaryDirectory /var/tmp"), ]) error_message = "clamd's AppArmor profile allows its TemporaryDirectory, /var/tmp, or every scan errors" } assert { condition = strcontains(local.init_script, "sed -i '/^TCPSocket[[:space:]]/d;/^TCPAddr[[:space:]]/d' \"$conf\"") error_message = "any TCPSocket or TCPAddr line the package wrote is deleted" } assert { condition = !strcontains(local.init_script, "setkey TCPSocket") && !strcontains(local.init_script, "setkey TCPAddr") error_message = "clamd itself never listens on TCP; the gated proxy does" } assert { condition = output.stream_max_bytes == 26214400 error_message = "the console's CARLOS_CLAMD_MAX_BYTES comes from this output" } } run "small_box_with_concurrent_reload_is_refused" { command = plan variables { instance_type = "t4g.small" } expect_failures = [aws_instance.scanner] } run "small_box_without_concurrent_reload_is_fine" { command = plan variables { instance_type = "t4g.small" concurrent_database_reload = false } assert { condition = strcontains(local.init_script, "setkey ConcurrentDatabaseReload no") error_message = "the reload setting reaches clamd.conf" } } run "stream_limit_too_small_for_the_fixtures" { command = plan variables { stream_max_bytes = 1048576 } expect_failures = [var.stream_max_bytes] } run "stream_limit_above_the_scan_budget" { command = plan variables { stream_max_bytes = 209715200 } expect_failures = [var.stream_max_bytes] } run "stream_limit_not_a_whole_mib" { command = plan variables { stream_max_bytes = 26214401 } expect_failures = [var.stream_max_bytes] } run "the_units" { command = plan assert { condition = alltrue([ for line in [ "DefaultDependencies=no", "Requires=scanner-selftest.service", "BindsTo=scanner-selftest.service", "After=scanner-selftest.service", "ListenStream=@PRIVATE_IP@:3310", "FreeBind=yes", ] : strcontains(file("${path.module}/files/scanner-tcp.socket"), "\n${line}\n") ]) error_message = "the socket opts out of default dependencies, is bound to the self-test and listens on the private address only" } assert { condition = alltrue([ for line in [ "Requires=scanner-selftest.service", "BindsTo=scanner-selftest.service", "After=scanner-selftest.service", ] : strcontains(file("${path.module}/files/scanner-tcp.service"), "\n${line}\n") ]) error_message = "the proxy is bound to the self-test, so connections in flight are cut with it" } assert { condition = alltrue([ for line in [ "BindsTo=clamav-daemon.service", "After=clamav-daemon.service", "Wants=scanner-tcp.socket", "Type=oneshot", "RemainAfterExit=yes", "PrivateTmp=yes", ] : strcontains(file("${path.module}/files/scanner-selftest.service"), "\n${line}\n") ]) 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" } assert { # Comments may sit between the heredoc line and [Unit]; the Wants= line # must be the first line of the [Unit] section. condition = can(regex("10-scanner\\.conf < /dev/console; } 2>/dev/null || true") error_message = "the console copy of each line is best effort" } } run "freshclam_uses_its_packaged_config" { command = plan assert { condition = !strcontains(local.init_script, "freshclam --config-file") && !strcontains(local.init_script, "/run/freshclam") 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" } assert { condition = strcontains(local.init_script, "until freshclam; do") error_message = "the first signatures must come from plain freshclam with the packaged config" } } run "another_vpc_needs_a_subnet" { command = plan override_data { target = data.aws_vpc.client values = { default = false } } expect_failures = [aws_instance.scanner] }