CloudFormation provider
Scans a CloudFormation template (YAML or JSON), no live AWS
credentials required. Short-form intrinsics (!Ref, !Sub, !GetAtt,
!Join, !If, …) are normalized to their JSON-form equivalents at
parse time so rules operate on one uniform structure.
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 (template properties instead of boto3 list/describe). CF-* rules are CloudFormation-only and have no AWS-runtime analogue.
Producer workflow
pipeline_check --pipeline cloudformation --cfn-template path/to/template.yaml
# or point at a directory and every *.yml / *.yaml / *.json / *.template is scanned
pipeline_check --pipeline cloudformation --cfn-template infra/
All other flags (--output, --severity-threshold, --checks,
--standard, …) behave the same as with the AWS provider.
Intrinsic handling
CFN values may be literals ("ap-southeast-2"), booleans,
unresolved intrinsics ({"Ref": "Region"}, {"Fn::Sub": "..."}),
or condition references. The scanner follows two conventions:
- Anything not provably safe is treated as a potential offender
unless the rule explicitly skips unresolved values. For
instance,
is_true(value)returnsTrueonly forTrueor"true", so a template that hides aRefbehind a boolean-typed property is scored as if the flag were disabled. - Statically-reducible intrinsics are reduced before matching.
resolve_literal(value, parameters)incloudformation/base.pyevaluates{"Ref": "ParamName"}against the template'sParameters.<Name>.Default,{"Fn::Sub": "literal"}/{"Fn::Sub": "...${Var}..."}including the[template, {var-map}]form, and{"Fn::Join": [delim, [list]]}when every list item resolves. Rules that benefit (EB-002target ARNs,CF-003VPC IDs) call the resolver first and fall back to the old "skip unresolved" path only when the intrinsic references a pseudo-parameter (AWS::Region) or a runtime-dependent intrinsic (Fn::GetAtt,Fn::ImportValue,Fn::If).
Rule helpers (importable from cloudformation/base.py):
as_str(value)— literal-only accessor, returns""for intrinsics.resolve_literal(value, parameters)— tries to reduce every statically-resolvable intrinsic; returnsNonewhen it can't.is_true(value)— strict boolean gate.is_intrinsic(value)— predicate used byCF-002to skip intrinsic dicts entirely when walking for hard-coded secrets.
This matches cfn-lint and cfn-nag conventions and keeps findings useful under the common case where templates are parameterized.
Resource-type coverage
| Service | IDs | CloudFormation resources consumed |
|---|---|---|
| CodeBuild | CB-001…011 |
AWS::CodeBuild::Project, AWS::CodeBuild::SourceCredential |
| CodePipeline | CP-001…005, CP-007 |
AWS::CodePipeline::Pipeline |
| CodeDeploy | CD-001…003 |
AWS::CodeDeploy::DeploymentGroup |
| ECR | ECR-001…006 |
AWS::ECR::Repository, AWS::ECR::PullThroughCacheRule |
| IAM | IAM-001…006, IAM-008 |
AWS::IAM::Role, AWS::IAM::Policy, AWS::IAM::ManagedPolicy |
| PBAC | PBAC-001…003, PBAC-005 |
AWS::CodeBuild::Project, AWS::EC2::SecurityGroup |
| S3 | S3-001…005 |
AWS::S3::Bucket (artifact buckets discovered from pipelines) |
| CloudTrail | CT-001…003 |
AWS::CloudTrail::Trail |
| CloudWatch Logs | CWL-001…002 |
AWS::Logs::LogGroup |
| Secrets Manager | SM-001…002 |
AWS::SecretsManager::Secret, AWS::SecretsManager::RotationSchedule |
| CodeArtifact | CA-001…004 |
AWS::CodeArtifact::Domain, AWS::CodeArtifact::Repository |
| CodeCommit | CCM-002…003 |
AWS::CodeCommit::Repository (CCM-001 omitted, no equivalent CFN resource) |
| Lambda | LMB-001…004 |
AWS::Lambda::Function, AWS::Lambda::Url, AWS::Lambda::Permission |
| KMS | KMS-001…002 |
AWS::KMS::Key |
| SSM | SSM-001…002 |
AWS::SSM::Parameter |
| EventBridge | EB-001…002 |
AWS::Events::Rule (targets are inline) |
| Signer | SIGN-001 |
AWS::Lambda::Function.CodeSigningConfigArn, AWS::Signer::SigningProfile |
| CloudWatch | CW-001 |
AWS::CloudWatch::Alarm (namespace=AWS/CodeBuild, metric=FailedBuilds) |
| CFN-native | CF-001…003 |
AWS::IAM::AccessKey, stateful data-store types, AWS::EC2::Subnet |
Limitations
- Template-level only. Resources provisioned outside the
template (console, SDK, sister stacks) are not scanned — use
--pipeline awsalongside for a live view. - Intrinsics are not evaluated beyond the statically-reducible
forms above. A
Fn::GetAtt,Fn::ImportValue, orFn::Ifpasses through as an opaque dict; rules that require a literal value silently skip the finding rather than guess at the resolved shape. - No cross-stack resolution.
Fn::ImportValuereferences are preserved as-is; the exporting stack is not fetched. - Transform macros are ignored.
AWS::Serverless-2016-10-31(SAM) and custom transforms run server-side; the pre-transform resources are what the scanner sees.
What it covers
70 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-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 | |
| CF-001 | Template declares AWS::IAM::AccessKey (long-lived credential) | CRITICAL | |
| CF-002 | Stateful data-store resource carries a plaintext secret | CRITICAL | |
| CF-003 | CodeBuild project's VPC contains a public subnet | HIGH | |
| 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 | |
| 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 | 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 |
CA-001: CodeArtifact domain not encrypted with customer KMS CMK
Reads AWS::CodeArtifact::Domain.Properties.EncryptionKey. An empty value means anyone with codeartifact:Read* can read packages — the encryption key isn't a separate authorization boundary.
Recommended action
Set EncryptionKey on every AWS::CodeArtifact::Domain to a customer-managed CMK ARN. The default AWS-owned key can't be rotated or scoped by IAM.
CA-002: CodeArtifact repository has a public external connection
Reads AWS::CodeArtifact::Repository.Properties.ExternalConnections. Any value beginning with public: (e.g. public:npmjs) fetches packages directly from the public ecosystem with no intermediate scrub.
Recommended action
Route every ExternalConnections entry through a private mirror that caches and vets public packages, or scope 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.Properties.PermissionsPolicyDocument. Fires on any Allow statement that names a wildcard principal — wildcard at the domain level grants access to every repo in the domain.
Recommended action
Remove Principal: "*" (or Principal.AWS: "*") from every Allow statement in PermissionsPolicyDocument. Name the specific accounts and add an aws:PrincipalOrgID condition.
CA-004: CodeArtifact repo policy grants codeartifact: with Resource ''
Parses AWS::CodeArtifact::Repository.Properties.PermissionsPolicyDocument. 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 and resources instead of codeartifact:* with Resource: "*".
CB-001: Secrets in plaintext environment variables
Walks every AWS::CodeBuild::Project's Properties.Environment.EnvironmentVariables list. Flags any entry whose Type is PLAINTEXT (or absent — the CFN default) 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, JWTs — the same shared detector catalog GHA-008 uses).
Recommended action
Move secrets to AWS Secrets Manager or SSM Parameter Store and reference them via Type: SECRETS_MANAGER or Type: PARAMETER_STORE on the corresponding Environment.EnvironmentVariables entry.
CB-002: Privileged mode enabled
Reads AWS::CodeBuild::Project.Properties.Environment.PrivilegedMode. Privileged mode grants the build container root-level access to the Docker daemon on the host.
Recommended action
Set Environment.PrivilegedMode: false (or omit it; the CFN default is false). Where Docker-in-Docker is unavoidable, consider Kaniko or BuildKit and keep the buildspec under branch protection.
CB-003: Build logging not enabled
Reads both LogsConfig.CloudWatchLogs.Status and LogsConfig.S3Logs.Status. Without either, the build's stdout/stderr is captured only in the in-flight console view — audit and incident review have no record.
Recommended action
Enable at least one of LogsConfig.CloudWatchLogs.Status: ENABLED or LogsConfig.S3Logs.Status: ENABLED. CloudWatch is the easier default; pair S3 with an object-lock bucket for tamper-evident retention.
CB-004: No build timeout configured
Reads AWS::CodeBuild::Project.Properties.TimeoutInMinutes. 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 TimeoutInMinutes to a value matched to your real build duration (15–60 minutes is typical). Pair with a CloudWatch alarm on AWS/CodeBuild BuildDuration.
CB-005: Outdated managed build image
Matches Environment.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.Image to the latest aws/codebuild/standard:<major>.0 release. For custom or third-party images, pin by @sha256:<digest> rather than a mutable tag.
CB-006: CodeBuild source auth uses long-lived token
Reads Source.Type and Source.Auth.Type plus any AWS::CodeBuild::SourceCredential.{ServerType,AuthType} side resource. Fires when an external VCS source (GITHUB, GITHUB_ENTERPRISE, BITBUCKET) is authenticated with a long-lived credential.
Recommended action
Replace OAUTH / PERSONAL_ACCESS_TOKEN / BASIC_AUTH with an AWS CodeConnections (CodeStar) connection. Tokens stored via AWS::CodeBuild::SourceCredential or inline Source.Auth don't rotate.
CB-007: CodeBuild webhook has no filter_group
Unlike Terraform (where webhooks are a separate resource), CFN models the webhook as a property of AWS::CodeBuild::Project.Triggers. Reads Triggers.{Webhook,FilterGroups} and fires when a webhook is enabled with no filter groups.
Recommended action
Define Triggers.FilterGroups entries 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.Properties.Source.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 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.Image using the same shared image classifier the workflow providers use. Mutable tags let an upstream image swap execute on the next build with no template change.
Recommended action
Pin Environment.Image by @sha256:<digest> rather than a mutable tag. AWS-managed aws/codebuild/standard:N images are exempted.
CB-010: CodeBuild webhook allows fork-PR builds without actor filtering
Reads Triggers.FilterGroups[*]. 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 Triggers.FilterGroups entry 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.BuildSpec. The matcher looks for reverse-shell payloads, miner CLIs, secret-exfil patterns, and credential-grabbing one-liners.
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-002: CodeCommit repository not encrypted with customer KMS CMK
Reads AWS::CodeCommit::Repository.Properties.KmsKeyId. 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 KmsKeyId 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 Triggers[*].DestinationArn against the account id of the current stack (extracted from sibling resource ARNs when possible). A trigger whose destination lives in another account leaks repository activity outside the trust boundary.
Recommended action
Point Triggers[*].DestinationArn 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::DeploymentGroup.Properties.AutoRollbackConfiguration. The block needs Enabled: true AND "DEPLOYMENT_FAILURE" present in Events for the deployment group to self-heal.
Recommended action
Enable AutoRollbackConfiguration 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::DeploymentGroup.Properties.DeploymentConfigName. Fires when the value is CodeDeployDefault.AllAtOnce, LambdaAllAtOnce, or ECSAllAtOnce — these route every request to the new revision simultaneously.
Recommended action
Switch DeploymentConfigName 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::DeploymentGroup.Properties.AlarmConfiguration.{Enabled,Alarms}. Without an alarm list, error spikes or latency regressions from a release won't auto-halt the deployment.
Recommended action
Add CloudWatch alarms to AlarmConfiguration.Alarms and set AlarmConfiguration.Enabled: true. Pair with CD-001 — alarm-triggered rollback only fires when at least one alarm exists to monitor.
CF-001: Template declares AWS::IAM::AccessKey (long-lived credential)
Fires on every AWS::IAM::AccessKey in the template. CloudFormation writes the resulting SecretAccessKey to stack outputs — the secret is now in every stack update log and every DescribeStacks response.
Recommended action
Replace static keys with role-based access: an AWS::IAM::Role plus an AWS::IAM::OIDCProvider for CI, or an IAM role for service-to-service auth. Static keys live forever in stack outputs and any tool that ever read them.
CF-002: Stateful data-store resource carries a plaintext secret
Walks every string value of the stateful data-store resources (AWS::RDS::DBInstance, AWS::RDS::DBCluster, AWS::Redshift::Cluster, AWS::ElastiCache::ReplicationGroup, AWS::DocDB::DBCluster, AWS::Neptune::DBCluster, AWS::OpenSearchService::Domain, AWS::MemoryDB::Cluster). Fires when a string leaf matches a credential shape 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 '{{resolve:secretsmanager:…}}' at deploy time. Never literal-string a credential into a stateful resource — the value lives in the template, the stack history, and any drift detection report.
CF-003: CodeBuild project's VPC contains a public subnet
When AWS::CodeBuild::Project.Properties.VpcConfig.VpcId resolves to a concrete reference, walks every AWS::EC2::Subnet in the same VPC and fires if any has MapPublicIpOnLaunch: true.
Recommended action
Place CodeBuild projects in private subnets (MapPublicIpOnLaunch: 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.
CP-001: No approval action before deploy stages
Walks AWS::CodePipeline::Pipeline.Stages[*].Actions[*].ActionTypeId.Category. Fires when any Deploy action is reachable from the source without an intervening Approval action upstream.
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 ArtifactStore.EncryptionKey (or ArtifactStores for cross-region pipelines). An empty value means the store falls back to AWS-owned-key S3 SSE; with a CMK you control key policy and rotation independently.
Recommended action
Set ArtifactStore.EncryptionKey (or every entry in ArtifactStores.EncryptionKey) to a customer-managed KMS CMK. 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 Stages[*].Actions[*] where ActionTypeId.Category == "Source" and Configuration.PollForSourceChanges is the string "true".
Recommended action
Set Configuration.PollForSourceChanges: false on every Source action and create an EventBridge rule (or CodeStar connection) to drive change detection on commit.
CP-004: Legacy ThirdParty/GitHub source action (OAuth token)
Fires on Stages[*].Actions[*] whose ActionTypeId.Owner == "ThirdParty" AND ActionTypeId.Provider == "GitHub". The v1 GitHub action stores a long-lived OAuth token literally in the pipeline configuration.
Recommended action
Switch the source action to ActionTypeId.Owner: AWS + ActionTypeId.Provider: CodeStarSourceConnection and point Configuration.ConnectionArn at an AWS::CodeStarConnections::Connection.
CP-005: Production Deploy stage has no preceding ManualApproval
A stricter version of CP-001 scoped to production-named stages. Walks Stages[*].Name for prod / production / live substrings and requires a preceding Approval action.
Recommended action
Add a Manual approval action in the stage that precedes any stage whose name contains prod / production / live and contains a Deploy action.
CP-007: CodePipeline v2 PR trigger accepts all branches
Inspects v2 pipelines (PipelineType: V2) whose Triggers[*].GitConfiguration declares a PullRequest block without Branches.Includes.
Recommended action
Set Triggers[*].GitConfiguration.PullRequest[*].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::Trail resources. Without a trail (declared here or out-of-band), management-plane activity has no durable audit record.
Recommended action
Declare at least one AWS::CloudTrail::Trail — typically a single IsMultiRegionTrail: true trail sending events to a write-protected S3 bucket. If trails are managed out-of-band, baseline this rule's INFO emission.
CT-002: CloudTrail log-file validation disabled
Reads AWS::CloudTrail::Trail.Properties.EnableLogFileValidation. 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 EnableLogFileValidation: true on every AWS::CloudTrail::Trail. CloudTrail then writes hash digests S3 cannot tamper with, post-incident validation can detect log forgery.
CT-003: CloudTrail trail is not multi-region
Reads AWS::CloudTrail::Trail.Properties.IsMultiRegionTrail. 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 IsMultiRegionTrail: true so a single trail captures activity from every region. A region-scoped trail misses anything an attacker does in another region.
CW-001: No CloudWatch alarm on CodeBuild FailedBuilds metric
Gated check: fires only when the template declares AWS::CodeBuild::Project. Passes when at least one AWS::CloudWatch::Alarm is configured for Namespace: AWS/CodeBuild + MetricName: FailedBuilds.
Recommended action
Declare an AWS::CloudWatch::Alarm with Namespace: AWS/CodeBuild and MetricName: 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::Logs::LogGroup by LogGroupName prefix /aws/codebuild/ and reads RetentionInDays. Unbounded retention isn't free; it also makes incident response harder when there are years of irrelevant logs to grep.
Recommended action
Set RetentionInDays on every AWS::Logs::LogGroup whose name starts with /aws/codebuild/. 30 / 90 / 365 days are typical.
CWL-002: CodeBuild log group not KMS-encrypted
Reads AWS::Logs::LogGroup.Properties.KmsKeyId 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 KmsKeyId on every AWS::Logs::LogGroup 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::Events::Rule whose EventPattern JSON matches aws.codepipeline Pipeline Execution State Change events filtered to FAILED.
Recommended action
Declare an AWS::Events::Rule whose EventPattern matches the CodePipeline Pipeline Execution State Change detail-type 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::Events::Rule.Properties.Targets[*].Arn. A literal * in the ARN is the offending shape — it makes the target opaque to any reviewer tracing event flow.
Recommended action
Pin AWS::Events::Rule.Targets[*].Arn to a specific function or queue ARN. Wildcards in target ARNs 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.Properties.ImageScanningConfiguration.ScanOnPush. Without it, a freshly-pushed image goes straight into deployable storage with no known-CVE pass.
Recommended action
Set ImageScanningConfiguration.ScanOnPush: 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.Properties.ImageTagMutability. Default is MUTABLE — anyone with ecr:PutImage on the repo can overwrite release tags consumed by production deployments.
Recommended action
Set ImageTagMutability: IMMUTABLE on every AWS::ECR::Repository. Immutable tags point at exactly one digest forever — an attacker can't swap :latest mid-deploy.
ECR-003: Repository policy allows public access
Parses AWS::ECR::Repository.Properties.RepositoryPolicyText (or the standalone resource if used). Flags any Allow statement that names a wildcard principal — 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.
ECR-004: No lifecycle policy configured
Reads AWS::ECR::Repository.Properties.LifecyclePolicy. Without one, images and untagged digests accumulate indefinitely — old vulnerable images stay deployable.
Recommended action
Configure LifecyclePolicy.LifecyclePolicyText with rules that expire untagged and old tagged images. Bounded image age and bounded image count are reasonable starting points.
ECR-005: Repository encrypted with AES256 rather than KMS CMK
Reads AWS::ECR::Repository.Properties.EncryptionConfiguration.{EncryptionType,KmsKey}. 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 EncryptionConfiguration.EncryptionType: KMS and EncryptionConfiguration.KmsKey: <CMK ARN> referencing a customer-managed CMK with a key policy scoped to the principals that legitimately pull.
ECR-006: ECR pull-through cache rule uses an untrusted upstream
Reads AWS::ECR::PullThroughCacheRule.Properties.{UpstreamRegistryUrl,CredentialArn}. Fires when the upstream is not on the trusted allow-list AND no credential ARN is configured.
Recommended action
Either scope UpstreamRegistryUrl to a trusted registry (public.ecr.aws, registry.k8s.io, ghcr.io, gcr.io) or set CredentialArn so the upstream authenticates the pull.
IAM-001: CI/CD role has AdministratorAccess policy attached
Considers a role CI/CD-scoped when its AssumeRolePolicyDocument trusts codebuild.amazonaws.com, codepipeline.amazonaws.com, or codedeploy.amazonaws.com. Reads ManagedPolicyArns literal entries; 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 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 Role.Policies plus the resolved AWS::IAM::ManagedPolicy referenced via ManagedPolicyArns: { Ref: … }. Fires when any Allow statement names "*" in Action.
Recommended action
Enumerate the specific IAM actions the role needs and drop Action: "*" entirely. Access Analyzer or CloudTrail-based policy generation can suggest a minimum set.
IAM-003: CI/CD role has no permission boundary
Reads AWS::IAM::Role.Properties.PermissionsBoundary. Without a boundary, every additive policy attached to the role takes immediate effect — there's no second layer constraining the maximum reach.
Recommended action
Set PermissionsBoundary on every CI/CD role to a managed policy ARN (or { Ref: <ManagedPolicy> }). Boundaries cap effective permissions 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 the canonical AWS privilege-escalation primitive.
Recommended action
Scope iam:PassRole to the specific role ARNs the pipeline must hand off to. 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 AssumeRolePolicyDocument. Walks every Allow statement whose Principal.AWS is an external account, and fires when no Condition 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 that pairs a sensitive service action (s3:*, kms:*, secretsmanager:*, ssm:*, iam:*, sts:*, dynamodb:*, lambda:*, ec2:*) with Resource: "*".
Recommended action
Scope Resource to specific ARNs (bucket ARNs, key ARNs, secret ARNs, role ARNs). Reserve Resource: "*" for actions that genuinely require it (ec2:Describe*, cloudwatch:DescribeAlarms).
IAM-008: OIDC-federated role trust policy missing audience or subject pin
Inspects every AWS::IAM::Role.Properties.AssumeRolePolicyDocument 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).
KMS-001: Customer-managed symmetric KMS key has rotation disabled
Reads AWS::KMS::Key.Properties.EnableKeyRotation on symmetric keys (KeySpec = SYMMETRIC_DEFAULT or absent). Asymmetric keys are skipped — KMS doesn't rotate them, key replacement is the only path.
Recommended action
Set EnableKeyRotation: 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.Properties.KeyPolicy. 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.Properties.CodeSigningConfigArn. 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 CodeSigningConfigArn on every AWS::Lambda::Function to an AWS::Lambda::CodeSigningConfig whose allowed publishers list signing profiles your release pipeline uses.
LMB-002: Lambda Function URL configured with AuthType = NONE
Reads AWS::Lambda::Url.Properties.AuthType. The NONE setting exposes the function over a public HTTPS endpoint with no authentication.
Recommended action
Set AuthType: AWS_IAM on every AWS::Lambda::Url and grant invoke via explicit AWS::Lambda::Permission resources rather than leaving the URL public.
LMB-003: Lambda environment variables contain plaintext secrets
Walks AWS::Lambda::Function.Properties.Environment.Variables for (a) secret-like names (PASSWORD, TOKEN, API_KEY) and (b) credential-shaped values (AKIA…, ghp_…, xox*, JWTs).
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 KmsKeyArn.
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 the role it executes with — to the whole internet.
Recommended action
Drop any AWS::Lambda::Permission with Principal: "*". Name the specific service principal or account that needs invoke, and scope further with SourceAccount / SourceArn conditions.
PBAC-001: CodeBuild project has no VPC configuration
Reads AWS::CodeBuild::Project.Properties.VpcConfig.{VpcId,Subnets,SecurityGroupIds}. All three must be set. Without VPC config, build nodes run in AWS-managed infrastructure with unrestricted outbound internet.
Recommended action
Set VpcConfig.VpcId, VpcConfig.Subnets, and VpcConfig.SecurityGroupIds 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.ServiceRole collisions (Ref / Fn::GetAtt references are resolved to the target logical id so identical-target references coalesce). When two or more projects point at the same role, a build compromise in any one inherits the others' permissions.
Recommended action
Create one AWS::IAM::Role per AWS::CodeBuild::Project and reference it via ServiceRole. Per-project roles cap the blast radius of a hijacked build.
PBAC-003: Security group allows 0.0.0.0/0 all-port egress
Walks AWS::EC2::SecurityGroup.Properties.SecurityGroupEgress for every AWS::EC2::SecurityGroup in the template (not only CodeBuild-attached ones). Fires on any rule that allows 0.0.0.0/0 (or ::/0) on the full port range, a completely open exfiltration channel.
Recommended action
Scope egress to the specific destinations the build needs. Drop the catch-all SecurityGroupEgress: { CidrIp: 0.0.0.0/0, IpProtocol: -1 }.
PBAC-005: Pipeline action roles all equal the pipeline-level role
Compares each Stages[*].Actions[*].RoleArn against the pipeline's top-level RoleArn. When all action-level values are empty or identical to the pipeline role, every stage runs with the same blast-radius.
Recommended action
Assign a least-privilege RoleArn to every Stages[*].Actions[*] that needs cross-account or cross-service permissions, instead of falling back to the pipeline's top-level RoleArn.
S3-001: Artifact bucket public access block not fully enabled
Discovers pipeline artifact buckets via ArtifactStore.Location / ArtifactStores[*].Location and reads AWS::S3::Bucket.Properties.PublicAccessBlockConfiguration. Any of the four PAB flags left false (or missing) lets an ACL or bucket policy expose build artifacts.
Recommended action
Set PublicAccessBlockConfiguration.{BlockPublicAcls,IgnorePublicAcls,BlockPublicPolicy,RestrictPublicBuckets} all to true on every artifact bucket.
S3-002: Artifact bucket server-side encryption not configured
Reads AWS::S3::Bucket.Properties.BucketEncryption.ServerSideEncryptionConfiguration[0].ServerSideEncryptionByDefault.SSEAlgorithm.
Recommended action
Configure BucketEncryption.ServerSideEncryptionConfiguration with ServerSideEncryptionByDefault.SSEAlgorithm: aws:kms and KMSMasterKeyID set to a customer-managed CMK.
S3-003: Artifact bucket versioning not enabled
Reads AWS::S3::Bucket.Properties.VersioningConfiguration.Status — must be Enabled.
Recommended action
Set VersioningConfiguration.Status: Enabled on 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
Reads AWS::S3::Bucket.Properties.LoggingConfiguration.DestinationBucketName.
Recommended action
Set LoggingConfiguration.DestinationBucketName to 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
Looks for an AWS::S3::BucketPolicy joined to the artifact bucket by Bucket (literal name or { Ref: <BucketLogicalId> }). Parses the policy and scans for any Deny statement whose Condition matches aws:SecureTransport = false.
Recommended action
Attach an AWS::S3::BucketPolicy carrying a Deny statement on Action: "s3:*" when Bool aws:SecureTransport = false.
SIGN-001: No active AWS Signer profile exists for the Lambda platform
Gated check: fires only when an AWS::Lambda::Function references CodeSigningConfigArn. Passes when at least one AWS::Signer::SigningProfile with PlatformId containing AWSLambda exists in the template.
Recommended action
Declare an AWS::Signer::SigningProfile with PlatformId: AWSLambda-SHA384-ECDSA and reference it from an AWS::Lambda::CodeSigningConfig. Without one, Lambda code signing has no signer to validate against (see LMB-001).
SM-001: Secrets Manager secret has no rotation configured
Joins AWS::SecretsManager::RotationSchedule to AWS::SecretsManager::Secret by SecretId. Fires when a secret has no matching rotation resource — a static secret lives forever in any backup or snapshot taken since the leak.
Recommended action
Declare an AWS::SecretsManager::RotationSchedule that targets the secret via SecretId (literal ARN or { Ref: <SecretLogicalId> }), with HostedRotationLambda or a RotationLambdaARN plus RotationRules.AutomaticallyAfterDays.
SM-002: Secrets Manager resource policy allows wildcard principal
Parses AWS::SecretsManager::ResourcePolicy.Properties.ResourcePolicy. 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 on AWS::SecretsManager::ResourcePolicy. 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.Properties.Name against the standard secret-name regex. If the name matches and Type is String (the CFN-only default — SecureString is not creatable via CFN, see AWS docs), the value is in plaintext.
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.Properties.{Type,KeyId}. Fires on a SecureString whose KeyId is empty or set to alias/aws/ssm.
Recommended action
Set KeyId 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.
Adding a new CloudFormation check
- Drop a single module at
pipeline_check/core/checks/cloudformation/rules/<id>_<slug>.pyexporting aRULE(metadata) and acheck(ctx: CloudFormationContext) -> list[Finding]callable. The orchestrator (CloudFormationRuleChecks) auto-discovers it and this doc's table picks it up on the next regen. - If the rule needs side resources (managed-policy dereferencing,
artifact-bucket discovery, policy documents joined on
Bucket/RoleName), add a private helper topipeline_check/core/checks/cloudformation/rules/_<service>_context.pyfollowing the_iam_context.py/_s3_context.pypattern. - 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/cloudformation/test_<service>.pyusingmake_cfn_ctxor one of the existing template fixtures. - (Recommended) Add an AWS-runtime parity rule under
pipeline_check/core/checks/aws/rules/and a Terraform parity rule underpipeline_check/core/checks/terraform/rules/so the three IaC entry points stay aligned. - Regenerate this doc: