SAST scanner · reads the file text

IaC Scanner

Infrastructure as code turns a cloud account into a text file, so a misconfiguration is a line you can search for. This scanner reads every .tf and .tfvars file and runs eight AWS-oriented regular expressions over the raw text: open security groups, wildcard IAM, secrets in variable defaults, public buckets, and a few others.

SASTLow to CriticalInfrastructure Misconfiguration8 checksTerraform, AWS

Which files it reads

The simplest discovery in the group: file extension only.

Path in the repositoryDecisionGoes to infra/main.tf extension .tfTerraform checks (8) infra/variables.tf extension .tfTerraform checks (8) infra/prod.tfvars extension .tfvars, where secrets often landTerraform checks (8) modules/vpc/main.tf.json ends in .json, not .tfnot scanned cfn/stack.yaml CloudFormation is not readnot scanned terraform.tfstate state files are not readnot scanned infra/README.md not infrastructure codenot scanned

1The scanner looks at the repository file index. Infrastructure as Code has a clear file extension, so extension is the only test.

2Files ending in .tf or .tfvars are Terraform. Every one gets all eight checks, with no per-file routing. The .tfvars files matter because that is where people paste real values.

3Everything else is passed over. CloudFormation templates are not read, nor are JSON Terraform, state files or documentation.

Walking a Terraform file

Eight checks, nine rules. The IAM check reports actions and resources as separate findings.

infra/main.tfread as one string, never parsed 1variable "stripe_key" { 2 default = "sk_live_abc123def456" 3} 4resource "aws_security_group" "db" { 5 ingress { 6 from_port = 5432 7 cidr_blocks = ["0.0.0.0/0"] 8 } 9} 10resource "aws_s3_bucket" "data" { 11 acl = "public-read" 12} 13resource "aws_iam_policy" "p" { 14 policy = jsonencode({ Statement = [{ "Action" : "*", "Resource" : "*" }] }) 15} 16resource "aws_secretsmanager_secret" "s" { 17 recovery_window_in_days = 0 18} 19resource "aws_cloudwatch_log_group" "l" { 20 retention_in_days = 3 21} Checks Open ingress on a non-web portHigh · conf 0.90 FLAG IAM Action: *High · conf 0.90 FLAG IAM Resource: *Medium · conf 0.90 FLAG Secret in a variable defaultCritical · conf 0.95 FLAG S3 bucket with public ACLHigh · conf 0.90 FLAG Secret deleted with no recoveryMedium · conf 0.80 FLAG ALB forwards HTTPMedium · conf 0.85 clear ECS task with a public IPLow · conf 0.70 clear Short log retentionLow · conf 0.65 FLAG High, conf 0.90. The title carries the port: "Security group allows 0.0.0.0/0 ingress on port 5432". High, conf 0.90. Medium, conf 0.90. Same line, second finding. Critical, conf 0.95. One finding per match. High, conf 0.90. Anyone on the internet can read every object. Medium, conf 0.80. Low, conf 0.65. Real output for this file: 7 findings.

1One Terraform file, twenty-one lines. Eight checks run in a fixed order over the raw text; the HCL is never parsed. Two checks (ALB and ECS) will find nothing here.

2Security group. A regular expression finds an ingress { block that holds 0.0.0.0/0 (or ::/0), then reads from_port out of the same block. Ports 80 and 443 are allowed; 5432 is not.

3IAM action. "Action" : "*" in JSON form, actions = ["*"] or action = "*" in HCL. The policy can call every AWS API.

4IAM resource. The same shapes for Resource. Reported separately and one level lower, because a wildcard resource with narrow actions is less dangerous.

5Hardcoded secret. A default = value shaped like a Stripe live key, Anthropic key, AWS access key, GitHub token, OpenAI key, a database URL or a JWT. The value must look like the secret; a word like changeme does not match.

6Public S3. acl = "public-read" or public-read-write anywhere in the file.

7Secrets Manager. recovery_window_in_days = 0 deletes a secret immediately and permanently. A mistake cannot be undone.

8Log retention. Any retention_in_days of 7 or fewer is flagged. The title carries the number.

9Seven findings. The ALB and ECS checks stayed quiet because this file has neither a plain HTTP forward nor assign_public_ip = true. Missing encryption and missing public access blocks are not checked.

What it deliberately skips

Each row is a line the scanner meets. Step through for the verdict and the reason.

What the scanner meetsVerdictWhy from_port = 443ingress block open to 0.0.0.0/0SKIP Ports 80 and 443 may be public. Only other ports are reported. cidr_blocks = ["0.0.0.0/0"]ingress block with no from_port lineSKIP The port is read from the same block. With no port found the match is dropped. from_port = 22 / ipv6_cidr_blocks = ["::/0"]ingress block open to the IPv6 worldFLAG IPv6 has its own pattern, so ::/0 on a non-web port is reported like 0.0.0.0/0. retention_in_days = 7CloudWatch log groupFLAG The threshold is 7 and the test is less-than-or-equal. 30 or 90 days would pass. default = "changeme"variable defaultSKIP Only known key shapes match: sk_live_, AKIA..., ghp_..., sk-ant-..., database URLs, JWTs. default = "AKIAIOSFODNN7EXAMPLE"variable defaultFLAG AKIA followed by 16 upper-case letters or digits is an AWS access key id. protocol = "HTTP" ... type = "forward"ALB listener, file also has "redirect" or "HTTPS"SKIP If the same file mentions an HTTPS listener or a redirect action, the forward is assumed conditional.

1Web ports are allowed. The check exists to catch databases, SSH and application ports, not the load balancer's front door.

2No port, no finding. The port is part of the title and the decision. A block without a from_port is skipped, which can hide an all-ports rule.

3IPv6 is covered. The IPv4 and IPv6 patterns are separate, so opening a port to ::/0 is not a way around the check.

4Seven days or fewer is short. The scanner suggests 30 to 90 days for investigation and compliance.

5Only known secret shapes. A weak password in a default is not detected here. This keeps the check at Critical, conf 0.95, without noise.

6A real key shape does match. This is the documented AWS example key id, and it is flagged all the same.

7The ALB check is file-wide. A redirect or an HTTPS listener anywhere in the same file turns the finding off, even if it belongs to another load balancer.

The fix, line by line

The fixed form is the one the regular expressions do not match.

Line the check matched, and the line after the fix findings in this file: 7 findings in this file: 6 findings in this file: 4 findings in this file: 3 findings in this file: 2 findings in this file: 1 findings in this file: 0 Open ingress on a non-web portcidr_blocks = ["0.0.0.0/0"]cidr_blocks = ["10.0.0.0/16"]FLAGclear IAM Action and Resource: *"Action" : "*", "Resource" : "*""Action" : "s3:GetObject", "Resource" : "arn:aws:s3:::app-data/*"FLAGclear Secret in a variable defaultdefault = "sk_live_abc123def456"# no default: pass TF_VAR_stripe_key or read from Secrets ManagerFLAGclear S3 bucket with public ACLacl = "public-read"acl = "private"FLAGclear Secret deleted with no recoveryrecovery_window_in_days = 0recovery_window_in_days = 7FLAGclear Short log retentionretention_in_days = 3retention_in_days = 90FLAGclear

1Before: the seven findings from the walk. Each step fixes one line and the matching checks go quiet.

2Narrow the source range. Only the load balancer or the VPC should reach a database port.

3Grant what is needed. One action, one resource ARN. Both IAM findings clear with this one edit.

4Remove the value from the file. The variable stays, the secret does not.

5Make the bucket private and put CloudFront in front if the objects should be public.

6Allow recovery. Seven days is the minimum the scanner accepts.

7Keep logs long enough to investigate. Six edits clear all seven findings.

Why it matters

Why it matters

Infrastructure misconfigurations are the root cause of most cloud data breaches. Public S3 buckets have leaked data from hundreds of companies. Overly permissive IAM roles let an attacker escalate from one compromised service to full account control. A secret committed in a Terraform default is in version control for good.

Real-world breaches

Twitch (2021). An AWS misconfiguration contributed to the leak of Twitch's entire source code and payout data. Accenture (2017). Four AWS S3 buckets were left public, exposing secret API data, authentication credentials and decryption keys.

Vulnerable vs fixed code

# BAD: the walk above
variable "stripe_key" {
  default = "sk_live_abc123def456"
}
resource "aws_security_group" "db" {
  ingress {
    from_port   = 5432
    cidr_blocks = ["0.0.0.0/0"]
  }
}
resource "aws_s3_bucket" "data" {
  acl = "public-read"
}

Checks and limits

Every check, with the severity and confidence the code assigns.

CheckFires whenSeverityConf.
Open ingressingress block with 0.0.0.0/0 or ::/0 and a port other than 80 or 443High0.90
IAM Action"Action" : "*", actions = ["*"], action = "*"High0.90
IAM Resourcethe same three shapes for resourceMedium0.90
Secret defaultdefault = value shaped like a known key, URL or JWTCritical0.95
Public S3acl of public-read or public-read-writeHigh0.90
Recovery windowrecovery_window_in_days = 0Medium0.80
ALB HTTP forwardprotocol = "HTTP" then type = "forward", with no "HTTPS" or "redirect" in the fileMedium0.85
ECS public IPassign_public_ip = trueLow0.70
Log retentionretention_in_days of 7 or fewerLow0.65
Docs and code

The scanner reads only .tf and .tfvars and does not check unencrypted storage, a missing public access block, disabled CloudTrail and flow logs, or default VPC use. It checks open security groups, secret defaults, Secrets Manager recovery, ALB HTTP, ECS public IPs and log retention. It is AWS only. Severity runs from Low to Critical. Findings carry the file path only.