Terraform provider
Two input paths, same rule pack:
- Plan JSON (canonical): fully resolved attributes from
terraform show -json. Every value is typed, no ambiguity. - HCL source (best-effort): direct
*.tfparsing viapython-hcl2. Variable/local substitution is partial; unresolvable references stay opaque and findings on those resources get confidence-demoted.
Every AWS-mirrored check ID (CB-, CP-, CD-, ECR-, IAM-, PBAC-, S3-, CT-, CWL-, SM-, CA-, CCM-, LMB-, KMS-, SSM-, EB-, SIGN-, CW-) maps one-to-one to its AWS-provider counterpart. The semantics are identical, only the data source differs. TF-* rules are Terraform-only and have no AWS-runtime analogue.
Plan JSON workflow (canonical)
terraform init
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
pipeline_check --pipeline terraform --tf-plan plan.json
HCL source workflow (no terraform binary required)
When main.tf is present and no --tf-plan is given, --tf-source .
is auto-detected. Variables with a default and locals with literal
values resolve; var.X / local.Y references without defaults stay
as opaque ${...} strings. Terraform functions (jsonencode,
lookup, coalesce) are not evaluated. Local child modules
(source = "./") are walked recursively; remote registry modules are
skipped.
All other flags (--output, --severity-threshold, --checks,
--standard, …) behave the same as with the AWS provider.
Child modules are walked recursively; mode = "data" entries are
exposed separately from managed resources so rules that only care
about to-be-created state keep their current semantics.
Per-check schema mapping
Every check reads Terraform's native attribute names (snake_case);
single-nested blocks appear as one-item lists. The summary table
below names the rule body's primary input; for the full per-attribute
path list, see the rule's source under
pipeline_check/core/checks/terraform/rules/.
CodeBuild (aws_codebuild_project)
| Check | Primary attribute(s) read |
|---|---|
| CB-001 | environment[0].environment_variable[*].{name,type,value} |
| CB-002 | environment[0].privileged_mode |
| CB-003 | logs_config[0].cloudwatch_logs[0].status, logs_config[0].s3_logs[0].status |
| CB-004 | build_timeout |
| CB-005 | environment[0].image (matched against aws/codebuild/standard:<major>.<minor>) |
| CB-006 | source[0].{type, auth[0].type} + aws_codebuild_source_credential.{server_type, auth_type} |
| CB-007 | aws_codebuild_webhook.{project_name, filter_group[*]} |
| CB-008 | source[0].buildspec (inline detection) |
| CB-009 | environment[0].image (digest-pin classifier) |
| CB-010 | aws_codebuild_webhook.filter_group[*].filter[*] |
| CB-011 | source[0].buildspec (IOC matcher) |
CodePipeline (aws_codepipeline)
| Check | Primary attribute(s) read |
|---|---|
| CP-001 | stage[*].action[*].category |
| CP-002 | artifact_store[*].encryption_key[*] |
| CP-003 | stage[*].action[*] where category = "Source" and configuration.PollForSourceChanges |
| CP-004 | stage[*].action[*] where owner = "ThirdParty" and provider = "GitHub" |
| CP-005 | Stages whose name matches prod / production / live |
| CP-007 | pipeline_type = "V2" + trigger.git_configuration.pull_request[*].branches.includes |
CodeDeploy (aws_codedeploy_deployment_group)
| Check | Primary attribute(s) read |
|---|---|
| CD-001 | auto_rollback_configuration[0].{enabled,events} |
| CD-002 | deployment_config_name |
| CD-003 | alarm_configuration[0].{enabled,alarms} |
ECR
| Check | Resource | Attribute(s) read |
|---|---|---|
| ECR-001 | aws_ecr_repository |
image_scanning_configuration[0].scan_on_push |
| ECR-002 | aws_ecr_repository |
image_tag_mutability |
| ECR-003 | aws_ecr_repository_policy |
policy (JSON, joined on repository) |
| ECR-004 | aws_ecr_lifecycle_policy |
presence (joined on repository) |
| ECR-005 | aws_ecr_repository |
encryption_configuration[0].{encryption_type,kms_key} |
| ECR-006 | aws_ecr_pull_through_cache_rule |
{upstream_registry_url,credential_arn} |
IAM (scoped to CI/CD service roles)
Scope filter: aws_iam_role.assume_role_policy includes
codebuild.amazonaws.com, codepipeline.amazonaws.com, or
codedeploy.amazonaws.com as a Service principal.
IAM-009 / IAM-010 are the cross-cloud OIDC-federation analogs
(Azure / GCP). They read their own resource types and are not gated by
the AWS service-principal scope filter above.
| Check | Primary input |
|---|---|
| IAM-001 | managed_policy_arns + aws_iam_role_policy_attachment.policy_arn |
| IAM-002 | inline + attached policy JSON (Action *) |
| IAM-003 | aws_iam_role.permissions_boundary |
| IAM-004 | inline + attached policy JSON (iam:PassRole on Resource = "*") |
| IAM-005 | aws_iam_role.assume_role_policy (external principal w/o sts:ExternalId) |
| IAM-006 | inline + attached policy JSON (sensitive actions on Resource = "*") |
| IAM-008 | aws_iam_role.assume_role_policy (OIDC :aud / :sub pin) |
| IAM-009 | azurerm_federated_identity_credential.{issuer,subject} |
| IAM-010 | google_iam_workload_identity_pool_provider (oidc.issuer_uri + attribute_condition) |
S3 (artifact buckets discovered from pipelines)
Discovery: walks every aws_codepipeline.artifact_store[*].location.
Per bucket, the helper resources below are joined by bucket name.
| Check | Helper resource | Attribute(s) read |
|---|---|---|
| S3-001 | aws_s3_bucket_public_access_block |
all four block_* / ignore_* / restrict_* flags |
| S3-002 | aws_s3_bucket_server_side_encryption_configuration |
rule[0].apply_server_side_encryption_by_default[0].sse_algorithm |
| S3-003 | aws_s3_bucket_versioning |
versioning_configuration[0].status |
| S3-004 | aws_s3_bucket_logging |
target_bucket |
| S3-005 | aws_s3_bucket_policy |
policy (JSON, Deny on aws:SecureTransport=false) |
Terraform-native (TF-*)
| Check | Primary input |
|---|---|
| TF-001 | aws_iam_access_key (any) |
| TF-002 | string leaves on aws_db_instance, aws_rds_cluster, aws_redshift_cluster, aws_elasticache_replication_group, aws_docdb_cluster, aws_neptune_cluster, aws_opensearch_domain, aws_memorydb_cluster |
| TF-003 | aws_codebuild_project.vpc_config[0].vpc_id + every aws_subnet in that VPC (map_public_ip_on_launch) |
Working with data sources
The context exposes a second iterator, ctx.data_sources(type=None),
for resources with mode = "data" (e.g. aws_iam_policy_document,
aws_caller_identity). Managed-resource iteration via
ctx.resources() is unchanged. In most plans, Terraform resolves
aws_iam_policy_document data sources inline, the rendered JSON
arrives on aws_iam_policy.policy or aws_iam_role_policy.policy
directly and the IAM checks see it without any extra work. The data
iterator only matters when the data source depends on a
to-be-created resource and Terraform defers it to apply.
Limitations
- Only the plan's resource set is visible. Resources provisioned outside Terraform (console, other stacks) are not scanned.
- No runtime state. Checks like ECR-003 that in AWS-provider mode
query the live repository policy rely here on whether an
aws_ecr_repository_policyresource exists in the plan.
What it covers
73 checks · 0 have an autofix patch (--fix).
| Check | Title | Severity | Fix |
|---|---|---|---|
| CA-001 | CodeArtifact domain not encrypted with customer KMS CMK | MEDIUM | |
| CA-002 | CodeArtifact repository has a public external connection | HIGH | |
| CA-003 | CodeArtifact domain policy allows cross-account wildcard | CRITICAL | |
| CA-004 | CodeArtifact repo policy grants codeartifact: with Resource '' | HIGH | |
| CB-001 | Secrets in plaintext environment variables | CRITICAL | |
| CB-002 | Privileged mode enabled | HIGH | |
| CB-003 | Build logging not enabled | MEDIUM | |
| CB-004 | No build timeout configured | LOW | |
| CB-005 | Outdated managed build image | MEDIUM | |
| CB-006 | CodeBuild source auth uses long-lived token | HIGH | |
| CB-007 | CodeBuild webhook has no filter_group | MEDIUM | |
| CB-008 | CodeBuild buildspec is inline (not sourced from a protected repo) | HIGH | |
| CB-009 | CodeBuild image not pinned by digest | MEDIUM | |
| CB-010 | CodeBuild webhook allows fork-PR builds without actor filtering | HIGH | |
| CB-011 | CodeBuild buildspec contains indicators of malicious activity | CRITICAL | |
| CCM-001 | CodeCommit repository has no approval rule template attached | HIGH | |
| CCM-002 | CodeCommit repository not encrypted with customer KMS CMK | MEDIUM | |
| CCM-003 | CodeCommit trigger targets SNS/Lambda in a different account | MEDIUM | |
| CD-001 | Automatic rollback on failure not enabled | MEDIUM | |
| CD-002 | AllAtOnce deployment config, no canary or rolling strategy | HIGH | |
| CD-003 | No CloudWatch alarm monitoring on deployment group | MEDIUM | |
| CP-001 | No approval action before deploy stages | HIGH | |
| CP-002 | Artifact store not encrypted with customer-managed KMS key | MEDIUM | |
| CP-003 | Source stage using polling instead of event-driven trigger | LOW | |
| CP-004 | Legacy ThirdParty/GitHub source action (OAuth token) | HIGH | |
| CP-005 | Production Deploy stage has no preceding ManualApproval | MEDIUM | |
| CP-007 | CodePipeline v2 PR trigger accepts all branches | HIGH | |
| CT-001 | No active CloudTrail trail in region | HIGH | |
| CT-002 | CloudTrail log-file validation disabled | MEDIUM | |
| CT-003 | CloudTrail trail is not multi-region | MEDIUM | |
| CW-001 | No CloudWatch alarm on CodeBuild FailedBuilds metric | LOW | |
| CWL-001 | CodeBuild log group has no retention policy | LOW | |
| CWL-002 | CodeBuild log group not KMS-encrypted | MEDIUM | |
| EB-001 | No EventBridge rule for CodePipeline failure notifications | MEDIUM | |
| EB-002 | EventBridge rule has a wildcard target ARN | HIGH | |
| ECR-001 | Image scanning on push not enabled | HIGH | |
| ECR-002 | Image tags are mutable | HIGH | |
| ECR-003 | Repository policy allows public access | CRITICAL | |
| ECR-004 | No lifecycle policy configured | LOW | |
| ECR-005 | Repository encrypted with AES256 rather than KMS CMK | MEDIUM | |
| ECR-006 | ECR pull-through cache rule uses an untrusted upstream | HIGH | |
| IAM-001 | CI/CD role has AdministratorAccess policy attached | CRITICAL | |
| IAM-002 | CI/CD role has wildcard Action in attached policy | HIGH | |
| IAM-003 | CI/CD role has no permission boundary | MEDIUM | |
| IAM-004 | CI/CD role can PassRole to any role | HIGH | |
| IAM-005 | CI/CD role trust policy missing sts:ExternalId | HIGH | |
| IAM-006 | Sensitive actions granted with wildcard Resource | MEDIUM | |
| IAM-008 | OIDC-federated role trust policy missing audience or subject pin | HIGH | |
| IAM-009 | Azure federated identity credential trusts a broad GitHub subject | HIGH | |
| IAM-010 | GCP workload identity provider has no repository attribute condition | HIGH | |
| KMS-001 | Customer-managed symmetric KMS key has rotation disabled | MEDIUM | |
| KMS-002 | KMS key policy grants kms:* to an IAM principal | HIGH | |
| LMB-001 | Lambda function has no code-signing config | HIGH | |
| LMB-002 | Lambda Function URL configured with AuthType = NONE | HIGH | |
| LMB-003 | Lambda environment variables contain plaintext secrets | HIGH | |
| LMB-004 | Lambda resource policy grants wildcard principal | CRITICAL | |
| PBAC-001 | CodeBuild project has no VPC configuration | HIGH | |
| PBAC-002 | CodeBuild service role shared across multiple projects | MEDIUM | |
| PBAC-003 | CodeBuild security group allows 0.0.0.0/0 all-port egress | MEDIUM | |
| PBAC-005 | Pipeline action roles all equal the pipeline-level role | HIGH | |
| S3-001 | Artifact bucket public access block not fully enabled | CRITICAL | |
| S3-002 | Artifact bucket server-side encryption not configured | HIGH | |
| S3-003 | Artifact bucket versioning not enabled | MEDIUM | |
| S3-004 | Artifact bucket access logging not enabled | LOW | |
| S3-005 | Artifact bucket missing aws:SecureTransport deny | MEDIUM | |
| SIGN-001 | No active AWS Signer profile exists for the Lambda platform | MEDIUM | |
| SM-001 | Secrets Manager secret has no rotation configured | HIGH | |
| SM-002 | Secrets Manager resource policy allows wildcard principal | CRITICAL | |
| SSM-001 | SSM parameter with secret-like name stored as String, not SecureString | HIGH | |
| SSM-002 | SecureString uses alias/aws/ssm rather than a customer CMK | MEDIUM | |
| TF-001 | Plan declares aws_iam_access_key (long-lived credential) | HIGH | |
| TF-002 | Stateful data-store resource carries a plaintext secret | CRITICAL | |
| TF-003 | CodeBuild VPC config references a public subnet | HIGH |
CA-001: CodeArtifact domain not encrypted with customer KMS CMK
Reads aws_codeartifact_domain.encryption_key. An empty value (or the default AWS-managed key) means anyone with codeartifact:Read* can read packages — the encryption key isn't a separate authorization boundary.
Recommended action
Set encryption_key on every aws_codeartifact_domain to a customer-managed KMS CMK ARN. The default AWS-owned key can't be rotated or scoped by IAM policy.
CA-002: CodeArtifact repository has a public external connection
Reads aws_codeartifact_repository.external_connections. Any value beginning with public: (e.g. public:npmjs) fetches packages directly from the public ecosystem with no intermediate scrub.
Recommended action
Route every aws_codeartifact_repository.external_connections through a private mirror that caches and vets public packages, or scope it with upstream allow-lists. Direct public:npmjs/public:pypi is dependency-confusion fuel.
CA-003: CodeArtifact domain policy allows cross-account wildcard
Parses aws_codeartifact_domain_permissions_policy.policy_document. Fires on any Allow statement that names a wildcard principal — wildcard at the domain level grants the bearer access to every repo in the domain.
Recommended action
Remove Principal: "*" (or Principal.AWS = "*") from every Allow statement in aws_codeartifact_domain_permissions_policy. Name the specific accounts and add an aws:PrincipalOrgID condition.
CA-004: CodeArtifact repo policy grants codeartifact: with Resource ''
Parses aws_codeartifact_repository_permissions_policy.policy_document. Fires when an Allow statement pairs codeartifact:* (or *) with Resource = "*". That combination lets the principal publish, delete, and rewrite every package version in the repo.
Recommended action
Enumerate specific actions (codeartifact:GetPackageVersion, codeartifact:DescribePackageVersion) and resources (specific package ARNs) instead of codeartifact:* with Resource = "*".
CB-001: Secrets in plaintext environment variables
Walks every aws_codebuild_project.environment[0].environment_variable[*]. Flags any entry whose type is PLAINTEXT (or absent, which Terraform defaults to PLAINTEXT) when (a) the name matches a secret-like pattern (PASSWORD, TOKEN, API_KEY, …) or (b) the value matches one of pipeline-check's known credential shapes (cloud access keys, VCS / registry / CI / cloud-service tokens, Slack xox* tokens, JWTs — the same shared detector catalog GHA-008 uses). Plaintext values land in the AWS console, CloudTrail, and build logs.
Recommended action
Move secrets to AWS Secrets Manager or SSM Parameter Store and reference them using type = "SECRETS_MANAGER" or type = "PARAMETER_STORE" on the corresponding environment_variable block.
CB-002: Privileged mode enabled
Reads aws_codebuild_project.environment[0].privileged_mode. Privileged mode hands the build container root-level access to the Docker daemon on the host. A compromised build can escape the container, modify other in-flight builds on the same host, or steal credentials mounted on the instance.
Recommended action
Disable environment[0].privileged_mode unless the project genuinely needs Docker-in-Docker. Where DinD is unavoidable, consider Kaniko or BuildKit's rootless mode and keep the buildspec under branch protection.
CB-003: Build logging not enabled
Reads both logs_config[0].cloudwatch_logs[0].status and logs_config[0].s3_logs[0].status. Without either, the build's stdout/stderr is captured only in the in-flight console view, audit and post-incident review have no record of what the build actually did.
Recommended action
Enable at least one of logs_config[0].cloudwatch_logs[0].status = "ENABLED" or logs_config[0].s3_logs[0].status = "ENABLED". CloudWatch is the easier default; pair S3 with an object-lock bucket if you need tamper-evident retention.
CB-004: No build timeout configured
Reads aws_codebuild_project.build_timeout (in minutes). Projects left at the AWS maximum of 480 minutes let a runaway or hijacked build consume compute and delay detection of a compromised pipeline stage.
Recommended action
Set build_timeout to a value matched to your real build duration (15–60 minutes is typical). Pair with a CloudWatch alarm on AWS/CodeBuild BuildDuration so builds that approach the cap surface as runtime alerts, not stuck jobs.
CB-005: Outdated managed build image
Matches environment[0].image against aws/codebuild/standard:<major>.<minor>. Older managed images carry unpatched OS packages, runtimes, and build tools, every artifact they produce inherits those gaps.
Recommended action
Update environment[0].image to the latest aws/codebuild/standard:<major>.0 release. For custom or third-party images, pin by @sha256:<digest> instead of a mutable tag (see CB-009).
CB-006: CodeBuild source auth uses long-lived token
Reads source[0].{type,auth[0].type} plus any aws_codebuild_source_credential.{server_type,auth_type} side resource. Fires when an external VCS source (GITHUB, GITHUB_ENTERPRISE, BITBUCKET) is authenticated with a long-lived OAuth/PAT/BASIC_AUTH credential.
Recommended action
Replace OAUTH / PERSONAL_ACCESS_TOKEN / BASIC_AUTH with an AWS CodeConnections (CodeStar) connection and reference it from source.location. Tokens stored via aws_codebuild_source_credential or inline source.auth don't rotate and survive the engineer who created them.
CB-007: CodeBuild webhook has no filter_group
Joins aws_codebuild_webhook records to their parent aws_codebuild_project via project_name and reads filter_group[*]. A webhook with no filter group accepts every push event from every principal, including forks for public repositories.
Recommended action
Define filter_group blocks on the aws_codebuild_webhook resource that restrict triggers to specific branches, actors, and event types. At minimum include an ACTOR_ACCOUNT_ID filter to keep fork PRs from triggering builds.
CB-008: CodeBuild buildspec is inline (not sourced from a protected repo)
Inspects aws_codebuild_project.source[0].buildspec. Flags multi-line literal values or values that begin with YAML preamble (version:, phases:) — those indicate an inline spec that any principal with codebuild:UpdateProject can rewrite without going through code review.
Recommended action
Move buildspec content into a buildspec.yml (or similar) inside the source repository, under branch protection. Reference it from source.buildspec only by relative path.
CB-009: CodeBuild image not pinned by digest
Classifies environment[0].image using the same shared image classifier the GitLab / Jenkins / Azure DevOps providers use. Mutable tags let an upstream image swap execute on the next build with no plan change.
Recommended action
Pin environment[0].image by @sha256:<digest> rather than a mutable tag. AWS-managed aws/codebuild/standard:N images are exempted (AWS owns the rotation contract).
CB-010: CodeBuild webhook allows fork-PR builds without actor filtering
Reads aws_codebuild_webhook.filter_group[*].filter[*]. For each group that covers a PULL_REQUEST_* event, fires when no sibling ACTOR_ACCOUNT_ID filter constrains the PR author.
Recommended action
Add an ACTOR_ACCOUNT_ID filter to every filter_group whose EVENT filter covers a PULL_REQUEST_* event. Without it, a fork-PR build runs with the project's service role.
CB-011: CodeBuild buildspec contains indicators of malicious activity
Runs the shared buildspec-IOC matcher against any inline source[0].buildspec. The matcher looks for reverse-shell payloads, miner CLIs, secret-exfil patterns, and credential-grabbing one-liners. Repo-sourced buildspecs are skipped — the text isn't visible in the plan.
Recommended action
Treat any hit on this rule as a potential pipeline compromise. Identify the commit that introduced the buildspec, rotate every credential reachable by the project's service role, and move the buildspec to a repo-sourced file under branch protection (see CB-008).
CCM-001: CodeCommit repository has no approval rule template attached
Looks for at least one aws_codecommit_approval_rule_template_association joined to the repository by repository_name. Without an approval rule, the merge gate every reviewer assumes exists doesn't.
Recommended action
Create an aws_codecommit_approval_rule_template requiring at least one reviewer from a named team, then associate it with the repository via aws_codecommit_approval_rule_template_association.
CCM-002: CodeCommit repository not encrypted with customer KMS CMK
Reads aws_codecommit_repository.kms_key_id. Empty values fall back to AWS-owned encryption, which can't be audited or scoped to a specific role via key policy.
Recommended action
Set kms_key_id on every aws_codecommit_repository to a customer-managed CMK ARN. Source code carries IP, credentials, and customer data — the encryption boundary matters.
CCM-003: CodeCommit trigger targets SNS/Lambda in a different account
Compares aws_codecommit_trigger.destination_arn against the current account ID (from aws_caller_identity data source). A trigger whose destination lives in another account leaks repository activity outside the trust boundary.
Recommended action
Point aws_codecommit_trigger.destination_arn at an SNS topic or Lambda function in the same account. If cross-account is intentional, document the receiving account in your threat model and baseline this finding.
CD-001: Automatic rollback on failure not enabled
Reads aws_codedeploy_deployment_group.auto_rollback_configuration[0]. The block needs enabled = true AND "DEPLOYMENT_FAILURE" present in events for the deployment group to self-heal.
Recommended action
Enable auto_rollback_configuration with at least the DEPLOYMENT_FAILURE event so a failed release returns the environment to its prior state without manual intervention.
CD-002: AllAtOnce deployment config, no canary or rolling strategy
Reads aws_codedeploy_deployment_group.deployment_config_name. Fires when the value is CodeDeployDefault.AllAtOnce, LambdaAllAtOnce, or ECSAllAtOnce — these route every request to the new revision simultaneously, leaving no canary validation window.
Recommended action
Switch deployment_config_name to a canary or linear config (e.g. CodeDeployDefault.LambdaCanary10Percent5Minutes). A staged rollout gives alarm-based rollback a window to catch regressions before they hit 100% of traffic.
CD-003: No CloudWatch alarm monitoring on deployment group
Reads aws_codedeploy_deployment_group.alarm_configuration[0].{enabled,alarms}. Without an alarm list, error spikes or latency regressions from a release won't auto-halt the deployment or trigger rollback.
Recommended action
Add CloudWatch alarms to alarm_configuration.alarms and set enabled = true. Pair this with CD-001 — alarm-triggered rollback only fires when at least one alarm exists to monitor.
CP-001: No approval action before deploy stages
Walks aws_codepipeline.stage[*].action[*].category. Fires when any Deploy action is reachable from the source without an intervening Approval action upstream of it.
Recommended action
Add a Manual approval action to a stage that precedes every Deploy-category action. Pipelines that auto-promote to production trust every prior stage's findings absolutely.
CP-002: Artifact store not encrypted with customer-managed KMS key
Reads every aws_codepipeline.artifact_store[*].encryption_key block. An empty list means the store falls back to AWS-owned-key S3 SSE; with a CMK you control key policy and rotation independently.
Recommended action
Set artifact_store[*].encryption_key to a customer-managed KMS CMK on every artifact store. Default S3 SSE is encrypted by an AWS-owned key you can't rotate or scope by IAM.
CP-003: Source stage using polling instead of event-driven trigger
Reads stage[*].action[*] where category = "Source". Fires when configuration.PollForSourceChanges is the literal string "true" — polling forces a 60s minimum trigger lag and bypasses the audit trail an EventBridge rule would leave.
Recommended action
Set configuration.PollForSourceChanges = "false" on every Source action and create an EventBridge rule (or aws_codestarconnections_connection) to drive change detection on commit.
CP-004: Legacy ThirdParty/GitHub source action (OAuth token)
Fires on any stage[*].action[*] with category = "Source", owner = "ThirdParty", provider = "GitHub". The v1 GitHub action authenticates with a long-lived OAuth token literally stored in the pipeline configuration, anyone with codepipeline:GetPipeline reads it.
Recommended action
Migrate the source action to owner = "AWS", provider = "CodeStarSourceConnection" and point configuration.ConnectionArn at an aws_codestarconnections_connection. The connection brokers short-lived OIDC credentials in place of the embedded OAuth token.
CP-005: Production Deploy stage has no preceding ManualApproval
A stricter version of CP-001 scoped to production-named stages. Walks stage[*].name for prod / production / live substrings and requires a preceding Approval action — even pipelines that pass CP-001 globally often skip the gate on the production stage.
Recommended action
Add a Manual approval action in the stage that precedes any stage whose name contains prod, production, or live and contains a Deploy action. Approval surfaces the release decision as an auditable event.
CP-007: CodePipeline v2 PR trigger accepts all branches
Inspects v2 pipelines (pipeline_type = "V2") whose trigger.git_configuration declares a pull_request block without branches.includes. The trigger then matches every PR, fork-source PRs included.
Recommended action
Set trigger.git_configuration.push[*].branches.includes or trigger.git_configuration.pull_request[*].branches.includes to the specific branches the pipeline expects. An empty include list runs on every branch event, including fork-PR rebases.
CT-001: No active CloudTrail trail in region
Counts aws_cloudtrail resources in the plan. Without a trail (declared here or out-of-band), management-plane activity has no durable audit record — every incident reply starts from scratch.
Recommended action
Declare at least one aws_cloudtrail resource — typically a single is_multi_region_trail = true trail sending events to a write-protected S3 bucket. If trails are managed out-of-band (e.g. Control Tower), this rule's INFO baseline is the right place to suppress it.
CT-002: CloudTrail log-file validation disabled
Reads aws_cloudtrail.enable_log_file_validation. Without it, an attacker with s3:PutObject on the trail's bucket can rewrite event records and there's no cryptographic record of the original.
Recommended action
Set enable_log_file_validation = true on every aws_cloudtrail resource. CloudTrail will then write hash digests S3 cannot tamper with, post-incident validation can detect log forgery.
CT-003: CloudTrail trail is not multi-region
Reads aws_cloudtrail.is_multi_region_trail for every declared trail. Multi-region is the only configuration that guarantees you'll see CreateAccessKey in ap-south-1 from your us-east-1 trail.
Recommended action
Set is_multi_region_trail = true so a single trail captures activity from every region. A region-scoped trail misses anything an attacker does in another region (a classic pivot).
CW-001: No CloudWatch alarm on CodeBuild FailedBuilds metric
Gated check: fires only when the plan declares aws_codebuild_project. Passes when at least one aws_cloudwatch_metric_alarm is configured for namespace = "AWS/CodeBuild" + metric_name = "FailedBuilds".
Recommended action
Declare an aws_cloudwatch_metric_alarm with namespace = "AWS/CodeBuild" and metric_name = "FailedBuilds" and route it to an actionable destination (PagerDuty, Slack via Chatbot, SNS topic with a human responder).
CWL-001: CodeBuild log group has no retention policy
Filters aws_cloudwatch_log_group by name prefix /aws/codebuild/ and reads retention_in_days. Unbounded retention isn't free; it also makes incident response harder when there are years of irrelevant logs to grep.
Recommended action
Set retention_in_days on every aws_cloudwatch_log_group whose name starts with /aws/codebuild/. 30 / 90 / 365 days are typical; match the figure to your compliance regime.
CWL-002: CodeBuild log group not KMS-encrypted
Reads aws_cloudwatch_log_group.kms_key_id on log groups whose name starts with /aws/codebuild/. Without a CMK, logs are encrypted with an AWS-owned key, which can't be audited or scoped by IAM.
Recommended action
Set kms_key_id on every aws_cloudwatch_log_group whose name starts with /aws/codebuild/ to a customer-managed CMK ARN. Build logs commonly carry secret fragments and environment dumps.
EB-001: No EventBridge rule for CodePipeline failure notifications
Looks for at least one aws_cloudwatch_event_rule whose event_pattern JSON matches aws.codepipeline Pipeline Execution State Change events filtered to FAILED. Without one, the only failure signal is engineers noticing the pipeline didn't update.
Recommended action
Declare an aws_cloudwatch_event_rule whose event_pattern matches aws.codepipeline events with detail.state = "FAILED", and target it at the notification destination of your choice (SNS, Slack via Chatbot, PagerDuty).
EB-002: EventBridge rule has a wildcard target ARN
Reads aws_cloudwatch_event_target.arn. A literal * in the ARN is the offending shape, even when EventBridge allows it at the API level, it makes the target opaque to any reviewer trying to trace event flow.
Recommended action
Pin aws_cloudwatch_event_target.arn to a specific function or queue ARN. Wildcards in target ARNs (e.g. arn:aws:lambda:*:*:function:*) defeat the per-target audit trail and let any resource matching the pattern receive the event.
ECR-001: Image scanning on push not enabled
Reads aws_ecr_repository.image_scanning_configuration[0].scan_on_push. Without it, a freshly-pushed image goes straight into deployable storage with no known-CVE pass.
Recommended action
Set image_scanning_configuration { scan_on_push = true } on every aws_ecr_repository. For deeper coverage, also enable Inspector v2 enhanced scanning at the registry level.
ECR-002: Image tags are mutable
Reads aws_ecr_repository.image_tag_mutability. Default is MUTABLE — anyone with ecr:PutImage on the repo can overwrite any existing tag, including release tags consumed by production deployments.
Recommended action
Set image_tag_mutability = "IMMUTABLE" on every aws_ecr_repository. With immutable tags, a tag points at exactly one digest forever; an attacker can't swap :latest mid-deploy.
ECR-003: Repository policy allows public access
Parses aws_ecr_repository_policy.policy JSON joined to the repo by repository. Flags any Allow statement that names a wildcard principal — a wildcard there lets every AWS account in the world pull the image.
Recommended action
Drop any Statement with Effect = "Allow" plus Principal = "*" (or Principal.AWS = "*" / Principal.Service = "*"). Use specific account IDs and lock cross-account access to a known set.
ECR-004: No lifecycle policy configured
Looks for an aws_ecr_lifecycle_policy joined by repository for each aws_ecr_repository. Without a lifecycle policy, images and untagged digests accumulate indefinitely — old vulnerable images stay deployable and storage costs creep.
Recommended action
Attach an aws_ecr_lifecycle_policy that expires untagged and old tagged images. Both bounded image age and bounded image count are reasonable starting points; pick what matches your release cadence.
ECR-005: Repository encrypted with AES256 rather than KMS CMK
Reads aws_ecr_repository.encryption_configuration[0].{encryption_type,kms_key}. The AES256 default uses an AWS-owned key — you can't audit who used it or revoke access with a key policy.
Recommended action
Set encryption_configuration { encryption_type = "KMS" kms_key = aws_kms_key.ecr.arn } referencing a customer-managed CMK with a key policy that scopes kms:Decrypt to the principals that legitimately pull.
ECR-006: ECR pull-through cache rule uses an untrusted upstream
Reads aws_ecr_pull_through_cache_rule.{upstream_registry_url,credential_arn}. Fires when the upstream is not on the trusted allow-list AND no credential ARN is configured — the cache then proxies any image from an attacker-controlled domain into your registry.
Recommended action
Either scope upstream_registry_url to a trusted registry (public.ecr.aws, registry.k8s.io, ghcr.io, gcr.io) or set credential_arn so the upstream registry authenticates the pull.
IAM-001: CI/CD role has AdministratorAccess policy attached
Considers a role CI/CD-scoped when its assume_role_policy trusts codebuild.amazonaws.com, codepipeline.amazonaws.com, or codedeploy.amazonaws.com. Reads managed_policy_arns plus every aws_iam_role_policy_attachment.policy_arn joined to the role, fires when arn:aws:iam::aws:policy/AdministratorAccess appears.
Recommended action
Replace AdministratorAccess with least-privilege policies that grant only the specific actions and resources the build actually needs. Pair with IAM-003 (permissions boundary) so a future policy edit can't quietly re-broaden the role.
IAM-002: CI/CD role has wildcard Action in attached policy
Walks every policy document attached to a CI/CD role: inline aws_iam_role_policy, inline blocks on the role itself, customer-managed aws_iam_policy joined through aws_iam_role_policy_attachment. Fires when any Allow statement names "*" in Action.
Recommended action
Enumerate the specific IAM actions the role needs and drop Action = "*" (or Action = ["*"]) entirely. Tools like Access Analyzer or CloudTrail-based policy generation can suggest the minimum set.
IAM-003: CI/CD role has no permission boundary
Reads aws_iam_role.permissions_boundary on every CI/CD-scoped role. Without a boundary, every additive policy attached to the role takes immediate effect — there's no second layer constraining the maximum reach.
Recommended action
Attach a permissions boundary policy via permissions_boundary = aws_iam_policy.cicd_boundary.arn. Boundaries cap the effective permissions of the role even if an admin later attaches a broader policy.
IAM-004: CI/CD role can PassRole to any role
Inspects every policy reachable from a CI/CD role. Fires on any Allow statement granting iam:PassRole (or iam:* / *) with Resource = "*". PassRole on a wildcard resource is one of the canonical privilege-escalation primitives in AWS.
Recommended action
Scope iam:PassRole to the specific role ARNs the pipeline must hand off to (CodeDeploy task role, ECS task role, …). Add an iam:PassedToService condition so the role can only be passed to the service that actually consumes it.
IAM-005: CI/CD role trust policy missing sts:ExternalId
Parses aws_iam_role.assume_role_policy. Walks every Allow statement whose Principal.AWS is an external account, and fires when no Condition on the statement carries sts:ExternalId. Without it the role is vulnerable to the confused-deputy pattern.
Recommended action
Add a Condition block with StringEquals.sts:ExternalId to every trust-policy statement that allows an external AWS account to assume the role. Generate a high-entropy ExternalId once and store it in the relying party's configuration.
IAM-006: Sensitive actions granted with wildcard Resource
Inspects every policy reachable from a CI/CD role. Fires on any Allow statement pairing a sensitive service action (s3:*, kms:*, secretsmanager:*, ssm:*, iam:*, sts:*, dynamodb:*, lambda:*, ec2:*) with Resource = "*". A compromised build with these reaches into prod data, secrets, and IAM in one step.
Recommended action
Scope Resource to specific ARNs (bucket ARNs, key ARNs, secret ARNs, role ARNs). Reserve Resource = "*" for actions that genuinely require it (e.g. ec2:Describe*, cloudwatch:DescribeAlarms).
IAM-008: OIDC-federated role trust policy missing audience or subject pin
Inspects every aws_iam_role.assume_role_policy that carries an OIDC trust statement (provider URL like token.actions.githubusercontent.com). Fires when Condition omits the audience or subject claim, or when a GitHub repo: subject wildcards the repo or ref segment (repo:org/*, repo:org/repo:*) or trusts the pull_request context. Without a specific repo + ref pin, an untrusted workflow (including a fork PR) can assume the role.
Recommended action
Add Condition.StringEquals (or StringLike) entries pinning both <host>:aud and <host>:sub to specific values. For GitHub Actions: pin aud to sts.amazonaws.com and sub to repo:<org>/<repo>:ref:refs/heads/main (or the env / branch combination the role expects).
IAM-009: Azure federated identity credential trusts a broad GitHub subject
Fires on an azurerm_federated_identity_credential whose issuer is the GitHub Actions OIDC issuer and whose subject wildcards the org/repo segment, wildcards the ref segment, or uses the pull_request context. Azure's Workload Identity Federation is the Azure analogue of the AWS OIDC trust IAM-008 audits; no other rule reads azurerm_federated_identity_credential. A subject pinned to a specific repo and ref/environment passes.
Recommended action
Pin azurerm_federated_identity_credential.subject to one repository AND a specific ref or environment, e.g. repo:myorg/myrepo:ref:refs/heads/main or repo:myorg/myrepo:environment:production. An org wildcard (repo:myorg/*), a ref wildcard (repo:myorg/myrepo:*), or the pull_request context lets an untrusted workflow run (including a fork pull request) exchange its GitHub token for your Azure identity. Use one federated credential per repo+environment rather than a wildcarded subject.
IAM-010: GCP workload identity provider has no repository attribute condition
Fires on a google_iam_workload_identity_pool_provider with an oidc block that either has no attribute_condition at all (any token from the issuer federates), or - for the GitHub / GitLab CI issuers - has a condition that never references the repository (repository / repo: / sub), so it does not constrain which repo can assume the identity. GHA-062 audits the same surface from a GitHub workflow's sibling files; this reads the Terraform resource directly.
Recommended action
Set attribute_condition on every google_iam_workload_identity_pool_provider with an oidc block, and make it constrain the source repository, e.g. assertion.repository_owner == 'myorg' or assertion.repository == 'myorg/myrepo'. Without a condition that pins the repo, any identity the issuer mints (any GitHub repo on the planet, for the GitHub issuer) can exchange its token for a Google access token scoped to whatever the pool grants. Restrict allowed_audiences as well.
KMS-001: Customer-managed symmetric KMS key has rotation disabled
Reads aws_kms_key.enable_key_rotation on symmetric keys (customer_master_key_spec = "SYMMETRIC_DEFAULT" or absent). Asymmetric keys are skipped — KMS doesn't rotate them, key replacement is the only path.
Recommended action
Set enable_key_rotation = true on every symmetric aws_kms_key. KMS rotates the underlying key material once per year transparently, no downstream change is needed.
KMS-002: KMS key policy grants kms:* to an IAM principal
Parses aws_kms_key.policy (or aws_kms_key_policy.policy). Fires on any Allow statement that pairs kms:* with a non-root IAM principal — that's the canonical key-compromise primitive.
Recommended action
Enumerate the specific KMS actions each principal needs (kms:Encrypt, kms:Decrypt, kms:GenerateDataKey, kms:DescribeKey). Reserve kms:* for the root principal that owns the key.
LMB-001: Lambda function has no code-signing config
Reads aws_lambda_function.code_signing_config_arn. Without it, Lambda accepts any zip the deployer can upload — there's no cryptographic check that the artifact came from the expected pipeline.
Recommended action
Set code_signing_config_arn on every aws_lambda_function to an aws_lambda_code_signing_config whose allowed publishers list signing profiles your release pipeline uses.
LMB-002: Lambda Function URL configured with AuthType = NONE
Reads aws_lambda_function_url.authorization_type. The NONE setting exposes the function over a public HTTPS endpoint with no authentication — if invoke is the goal, AWS_IAM with a scoped resource policy is almost always the right answer.
Recommended action
Set authorization_type = "AWS_IAM" on every aws_lambda_function_url and grant invoke permission via explicit aws_lambda_permission resources rather than leaving the URL public.
LMB-003: Lambda environment variables contain plaintext secrets
Walks aws_lambda_function.environment[0].variables for (a) secret-like names (PASSWORD, TOKEN, API_KEY) and (b) credential-shaped values (AKIA…, ghp_…, xox*, JWTs). Env vars are visible to anyone with lambda:GetFunctionConfiguration.
Recommended action
Move secrets to Secrets Manager or SSM Parameter Store and read them at function init time. For static values that must live in the env, encrypt them at rest with a customer CMK via kms_key_arn.
LMB-004: Lambda resource policy grants wildcard principal
Inspects every aws_lambda_permission resource. Fires when principal is "*" or any other wildcard form. A wildcard invoker exposes the function — and whatever role it executes with — to the whole internet.
Recommended action
Drop any aws_lambda_permission with principal = "*" (or principal = "arn:aws:iam::*:root"). Name the specific service principal or account that needs invoke, and scope further with source_account / source_arn conditions.
PBAC-001: CodeBuild project has no VPC configuration
Reads aws_codebuild_project.vpc_config[0].{vpc_id,subnets,security_group_ids}. All three must be set. Without VPC config, build nodes run in AWS-managed infrastructure with unrestricted outbound internet — every exfiltration path is open.
Recommended action
Set vpc_config { vpc_id = …, subnets = […], security_group_ids = […] } on every aws_codebuild_project. Use private subnets with egress scoped to the package mirrors and AWS endpoints the build actually needs.
PBAC-002: CodeBuild service role shared across multiple projects
Counts aws_codebuild_project.service_role collisions. When two or more projects share the same role ARN, a build compromise in any one of them inherits the others' permissions wholesale.
Recommended action
Create one aws_iam_role per aws_codebuild_project and reference it via service_role. Per-project roles cap the blast radius of a hijacked build to the resources that one project legitimately needs.
PBAC-003: CodeBuild security group allows 0.0.0.0/0 all-port egress
Walks aws_security_group.egress[*] for every SG attached to a CodeBuild project's vpc_config. Fires on any rule that allows 0.0.0.0/0 on the full port range — that's a completely open exfiltration channel.
Recommended action
Scope egress to the specific destinations the build needs (package mirrors, AWS endpoints via VPC interface endpoints). Drop the catch-all egress { cidr_blocks = ["0.0.0.0/0"], from_port = 0, to_port = 0, protocol = "-1" }.
PBAC-005: Pipeline action roles all equal the pipeline-level role
Compares each stage[*].action[*].role_arn against the pipeline's top-level role_arn. When all action-level values are empty or identical to the pipeline role, every stage runs with the same blast-radius — a compromise in any one action reaches the others' resources.
Recommended action
Assign a least-privilege role_arn to every stage[*].action[*] that needs cross-account or cross-service permissions, instead of falling back to the aws_codepipeline.role_arn.
S3-001: Artifact bucket public access block not fully enabled
Discovers pipeline artifact buckets from aws_codepipeline.artifact_store[*].location. For each, joins the corresponding aws_s3_bucket_public_access_block by bucket. Any of the four PAB flags left false (or missing entirely) lets an ACL or bucket policy make build artifacts publicly readable.
Recommended action
Attach an aws_s3_bucket_public_access_block with all four flags true to every artifact bucket: block_public_acls = true, ignore_public_acls = true, block_public_policy = true, restrict_public_buckets = true.
S3-002: Artifact bucket server-side encryption not configured
Discovers pipeline artifact buckets from aws_codepipeline.artifact_store[*].location and joins aws_s3_bucket_server_side_encryption_configuration by bucket. Reads rule[0].apply_server_side_encryption_by_default[0].sse_algorithm.
Recommended action
Attach an aws_s3_bucket_server_side_encryption_configuration with rule { apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" } } referencing a customer-managed KMS CMK.
S3-003: Artifact bucket versioning not enabled
Joins aws_s3_bucket_versioning by bucket for every pipeline artifact bucket. Reads versioning_configuration[0].status, passes only when it is Enabled.
Recommended action
Attach an aws_s3_bucket_versioning with versioning_configuration { status = "Enabled" } to every artifact bucket. Versioning lets you recover from accidental or malicious overwrites without restoring from external backups.
S3-004: Artifact bucket access logging not enabled
Joins aws_s3_bucket_logging by bucket for every pipeline artifact bucket. Passes when target_bucket is set on the joined resource.
Recommended action
Attach an aws_s3_bucket_logging resource pointing target_bucket at a central, write-protected logging bucket. Access logs are what forensics use to reconstruct who pulled which artifact during an incident.
S3-005: Artifact bucket missing aws:SecureTransport deny
Joins aws_s3_bucket_policy by bucket for every pipeline artifact bucket. Parses policy JSON and looks for any Deny statement whose Condition matches aws:SecureTransport = false. Without it, plaintext HTTP reads and writes still succeed.
Recommended action
Attach an aws_s3_bucket_policy carrying a Deny statement on Action: "s3:*" when Bool aws:SecureTransport = false. Validate the policy with Access Analyzer before applying.
SIGN-001: No active AWS Signer profile exists for the Lambda platform
Gated check: fires only when a aws_lambda_function references code_signing_config_arn. Passes when at least one aws_signer_signing_profile with platform_id starting with AWSLambda- exists in the plan.
Recommended action
Declare an aws_signer_signing_profile with platform_id = "AWSLambda-SHA384-ECDSA" and reference it from an aws_lambda_code_signing_config. Without one, the Lambda code-signing config can't be wired (see LMB-001).
SM-001: Secrets Manager secret has no rotation configured
Joins aws_secretsmanager_secret_rotation to aws_secretsmanager_secret by secret_id. Fires when a secret has no matching rotation resource — a static secret that lives forever in any backup or snapshot taken since the leak.
Recommended action
Declare an aws_secretsmanager_secret_rotation that targets the secret via its secret_id, with a Lambda rotation function and rotation_rules.automatically_after_days. 30 / 60 / 90-day cadences are the usual stops.
SM-002: Secrets Manager resource policy allows wildcard principal
Parses aws_secretsmanager_secret_policy.policy JSON and fires on any Allow statement that names a wildcard principal. The secret content is readable by every AWS account in the world until the policy is fixed.
Recommended action
Remove Principal: "*" (or Principal.AWS = "*") from every Allow statement in the resource policy. If cross-account access is intentional, name the specific accounts and add an aws:PrincipalOrgID condition.
SSM-001: SSM parameter with secret-like name stored as String, not SecureString
Checks aws_ssm_parameter.name against the standard secret-name regex (PASSWORD, TOKEN, API_KEY, …). If the name matches and type is String (the default), the value is stored in plaintext, visible to anyone with ssm:GetParameter.
Recommended action
Set type = "SecureString" on every aws_ssm_parameter whose name or value looks secret-like. SecureString parameters are encrypted with KMS and audited separately from plain GetParameter access.
SSM-002: SecureString uses alias/aws/ssm rather than a customer CMK
Reads aws_ssm_parameter.{type,key_id}. Fires on a SecureString whose key_id is empty or set to alias/aws/ssm — the encryption boundary collapses back to ssm:GetParameter permissions alone.
Recommended action
Set key_id on every SecureString aws_ssm_parameter to a customer-managed KMS CMK ARN. Default alias/aws/ssm is an AWS-owned key that can't be scoped or rotated by your key policy.
TF-001: Plan declares aws_iam_access_key (long-lived credential)
Fires on every aws_iam_access_key in the plan. Terraform writes the resulting secret to state, even on remote backends, the secret is now in every state-file backup, every CI run, and anywhere terraform output ran.
Recommended action
Replace static keys with role-based access: an aws_iam_role plus an OIDC aws_iam_openid_connect_provider for CI, or aws_iam_role for service-to-service auth. Static keys live forever in state, in backups, in every machine that ever ran terraform plan.
TF-002: Stateful data-store resource carries a plaintext secret
Walks every value of the stateful data-store resources (aws_db_instance, aws_rds_cluster, aws_redshift_cluster, aws_elasticache_replication_group, aws_docdb_cluster, aws_neptune_cluster, aws_opensearch_domain, aws_memorydb_cluster). Fires when a string leaf matches a credential shape (AKIA/ASIA, ghp_, JWT, …) OR when a secret-named attribute (*password, *token, …) carries a non-placeholder literal.
Recommended action
Move the secret into Secrets Manager (or SSM Parameter Store SecureString) and reference it via data.aws_secretsmanager_secret_version.… at apply time. Never literal-string a credential into a stateful resource — the value lives in state forever.
TF-003: CodeBuild VPC config references a public subnet
When aws_codebuild_project.vpc_config[0].vpc_id resolves to a concrete string, walks every aws_subnet in the same VPC and fires if any has map_public_ip_on_launch = true. Silent when vpc_id is unresolved (known after apply).
Recommended action
Place CodeBuild projects in private subnets (map_public_ip_on_launch = false) with egress routed through a NAT gateway or VPC interface endpoints. Public subnets put the build host on a public IP for the duration of the build.
Adding a new Terraform check
- Drop a single module at
pipeline_check/core/checks/terraform/rules/<id>_<slug>.pyexporting aRULE(metadata) and acheck(ctx: TerraformContext) -> list[Finding]callable. The orchestrator (TerraformRuleChecks) auto-discovers it and this doc's table picks it up on the next regen. - If the rule needs side resources (webhooks, attachments, policy
documents joined on
bucket/role), add a private helper topipeline_check/core/checks/terraform/rules/_<service>_context.pyfollowing the_iam_context.py/_s3_context.pypattern so the pre-fetch lands once per scan. - Add the check ID to
pipeline_check/core/standards/data/owasp_cicd_top_10.py(and any other standard that applies). - Add unit tests in
tests/terraform/test_<service>.pyusingmake_terraform_ctxor one of the existing plan fixtures. - (Recommended) Add an AWS-runtime parity rule under
pipeline_check/core/checks/aws/rules/so shift-left scans stay at parity with runtime. - Regenerate this doc: