Skip to content

feat(query): add OIDC trust policy misconfiguration queries for AWS IAM - #8023

Open
cx-antero-silva wants to merge 8 commits into
Checkmarx:masterfrom
cx-antero-silva:aws_iam_policy_document
Open

cx-antero-silva wants to merge 8 commits into
Checkmarx:masterfrom
cx-antero-silva:aws_iam_policy_document

Conversation

@cx-antero-silva

@cx-antero-silva cx-antero-silva commented Apr 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #6702

Adds two new Terraform/AWS queries to detect OIDC role trust policy misconfigurations between AWS IAM and CI/CD providers (GitHub Actions, GitLab CI, CircleCI, Bitbucket).

  • IAM Role OIDC Trust Missing Sub Condition (HIGH): Detects AssumeRoleWithWebIdentity trust policies with no condition restricting the OIDC sub (subject) claim, or a condition that only checks aud but not sub. Without this guard, any identity that can obtain a token from the OIDC provider can assume the role.

  • IAM Role OIDC Trust Wildcard Sub Condition (HIGH): Detects trust policies where StringLike is used on the sub claim with a value that wildcards the org/project segment — e.g. repo:* (any GitHub repo) or project_path:*:ref_type:branch:ref:* (any GitLab project on any branch). Allows repo:myorg/myrepo:ref:refs/heads/* as it scopes to a specific repo.

Both queries cover aws_iam_role (inline JSON heredoc) and data "aws_iam_policy_document" HCL data sources.

I submit this contribution under the Apache-2.0 license.

@cx-antero-silva
cx-antero-silva requested a review from a team as a code owner April 1, 2026 14:32
@cx-antero-silva cx-antero-silva self-assigned this Apr 1, 2026
…AM roles

Adds two new Terraform/AWS queries addressing GitHub issue Checkmarx#6702:

- IAM Role OIDC Trust Missing Sub Condition: detects AssumeRoleWithWebIdentity
  trust policies with no condition restricting the OIDC 'sub' claim, allowing
  any identity from the provider to assume the role.

- IAM Role OIDC Trust Wildcard Sub Condition: detects AssumeRoleWithWebIdentity
  trust policies where StringLike is used with a 'sub' value that wildcards the
  repository/project segment (e.g. 'repo:*', 'project_path:*:...'), allowing
  any CI/CD pipeline on the OIDC provider to assume the role.

Both queries cover aws_iam_role (inline JSON) and aws_iam_policy_document (HCL).
Supports GitHub Actions, GitLab CI, CircleCI, and Bitbucket OIDC providers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cx-antero-silva
cx-antero-silva requested a review from a team as a code owner September 29, 2026 15:16
@Santoshkumarpuppala

Copy link
Copy Markdown

At f71c0c8 the missing-sub query counts any condition key ending in :sub as a restriction, whatever the operator, so a trust policy whose only sub condition is StringNotEquals passes. The JSON path (query.rego:101-109) has no operator check, and the HCL path (query.rego:112-120) only checks that the variable ends in :sub.

I ran the two sub-condition helpers on their own under opa 1.18.0, not a full scan: StringNotEquals on the sub key returns true, same as StringEquals, while an aud-only condition correctly does not match. Excluding the StringNot* operators would fix it without flagging StringEqualsIgnoreCase. Happy to add a positive fixture if useful, or to be corrected if this is meant to count.

has_sub_condition_json/hcl counted any condition key ending in ":sub"
as a valid restriction regardless of operator, so a trust policy whose
only sub condition was StringNotEquals (or any other "Not" operator)
passed the check even though it only excludes specific subs instead of
restricting to an allow-list - every other identity from the OIDC
provider can still assume the role.

Now both paths also capture the operator and require it not contain
"Not", so StringNotEquals/StringNotLike no longer satisfy the check,
while StringEquals/StringEqualsIgnoreCase/StringLike still do.

Adds positive5.tf/positive6.tf (StringNotEquals bypass, JSON and HCL)
and negative3.tf/negative4.tf (StringEqualsIgnoreCase, confirming it's
still correctly treated as a valid restriction) per review feedback
from @Santoshkumarpuppala.
@cx-antero-silva

Copy link
Copy Markdown
Contributor Author

Good catch, thanks — fixed in 46f6341.

Both `has_sub_condition_json` and `has_sub_condition_hcl` now capture the operator alongside the key/variable and require it not contain `"Not"`, so `StringNotEquals`/`StringNotLike` no longer satisfy the check, while `StringEquals`/`StringEqualsIgnoreCase`/`StringLike` still do, matching what you found testing the helpers directly.

Added the fixtures you offered: `positive5.tf`/`positive6.tf` cover the `StringNotEquals` bypass (JSON and HCL paths), and `negative3.tf`/`negative4.tf` confirm `StringEqualsIgnoreCase` still correctly passes. Ran a full scan locally to confirm the new positives are caught and the negatives aren't.

@cx-artur-ribeiro

Copy link
Copy Markdown
Contributor

Hi @cx-antero-silva, I reviewed the queries and tested extra samples against them.

The proposed version is a single commit on top of your branch:

To take it: git fetch https://github.com/cx-artur-ribeiro/kics_fork_ai.git oidc-trust-review && git merge FETCH_HEAD (or git cherry-pick 10939607f6).

Proposed Changes:

  • Merge the two queries into a single one ("IAM Role OIDC Trust Unrestricted Sub Condition"): both share the same descriptionURL, action and principal type, as well as share most of the code in them. I didn't see a reason to have 2 queries instead of 1 here but let me know if you do not agree and want to keep the queries separate, adding the code improvements I suggested if you also agree. I left a comment above all query code and samples to identify clearly what the samples/code is doing;
  • I kept the distinction inside a single query, separating policies for the missing allow-listing condition and for the wildcard value, with their own messages -> any user still gets the correct remediation text; 4 policies (2 resource types x 2 scopes) instead of 8;
  • Library changes (additive fallbacks, existing callers keep the same behavior): common_lib.get_statement also accepts the lowercase statement, terraform_lib.get_action also accepts the lowercase actions/action, and new terraform_lib.get_condition;
  • Metadata: riskScore 6.0 from 8.5 (severity is High) and a merged descriptionText covering both cases;
  • Tests: 29 positive and 19 negative samples (previously 10 and 6), each one with a single line comment on what it tests.
New findings (reproduced with samples)
  • Action names are case-insensitive in AWS (IAM JSON policy elements: Action), but the code compares sts:AssumeRoleWithWebIdentity with == -> a lowercase action was not being flagged;
  • aws_iam_policy_document with more than one principals block (e.g. Service and Federated) -> not flagged, because lower() of an array is undefined;
  • The wildcard query only looked at StringLike -> StringLikeIfExists and ForAnyValue:StringLike were not flagged;
  • The regex only looked at the segment after the first colon -> a leading wildcard (*:ref:refs/heads/main) and ? as wildcard were not flagged (Condition operators);
  • After the Not fix, a role with only StringNotEquals over sub reported "has no 'sub' condition" but a sub condition exists;
  • Conditions on different operators are ANDed by AWS -> StringEquals with a specific sub plus StringLike with repo:* is restricted but was flagged as a wildcard.
  • The Null operator (and other non-string operators) over sub counted as a restrictive condition -> Null only checks if the key exists, it does not restrict who can assume the role;

I can share the full review document with the details if this is not enough.

As a follow-up note, while reviewing the PR I noticed that our shared library functions (get_statement and get_action) did not cover the full scope of the keys used by the HCL form (statement, actions, action, condition), which is why I proposed the library changes above.
They are additive, so other queries should not be affected and they can be reused by any query that has to handle both the JSON and the HCL policy formats.

Thanks for the effort on these queries!

cx-artur-ribeiro and others added 2 commits October 8, 2026 09:53
…verage

Merge "IAM Role OIDC Trust Missing Sub Condition" and "IAM Role OIDC Trust
Wildcard Sub Condition" into "IAM Role OIDC Trust Unrestricted Sub Condition".
The missing allow-listing condition and the wildcard value cases are kept as
separate policies with their own messages.

- add get_condition and lowercase actions/action to terraform get_action
- accept the lowercase "statement" key in common get_statement
- match the action case-insensitively, as a string or a list
- handle several "principals" blocks in aws_iam_policy_document
- only count string, non-negated operators as an allow-list (not Null, Bool)
- flag wildcards under StringLike, StringLikeIfExists and set-operator variants,
  including a leading wildcard and "?"
- treat a specific allow-listing sub as restrictive when ANDed with a wildcard
- set riskScore to 6.0 and merge the descriptions
- rework the test samples to cover JSON/HCL, string/array action, principals,
  statements, conditions and operators, each with a one line comment
The merged descriptionText from the OIDC trust query consolidation was
576 characters, exceeding the metadata schema's 500-char limit and
failing schema validation. Trimmed to 445 chars while keeping the
same substance (negated/non-string operators don't restrict; StringLike
wildcards are still too permissive).
@cx-antero-silva

Copy link
Copy Markdown
Contributor Author

Agreed on merging into a single query, and these are solid fixes — thanks for the thorough review and for preparing the branch, that made this easy to verify and take.

Cherry-picked 1093960 as-is (d0b4f42 is the only thing I added on top). Before taking it I traced through `sub_conditions`/`is_allow_listing`/`is_wildcard`/`has_restrictive_sub` against the JSON-multi-operator, HCL-single-block, and HCL-array-of-blocks cases to convince myself the AND-across-operators and Null/Not exclusions were handled correctly in all three shapes, and checked the `get_statement`/`get_action` library changes are genuinely additive (only reached via `else` when the existing JSON-key branches don't match) — ran the other ~30 queries that call them to confirm no behavior change there.

One issue I did find: the merged `descriptionText` was 576 chars, over the schema's 500-char limit, which would have failed `Validate Queries Metadata`. Trimmed it to 445 chars in d0b4f42, same substance.

Ran the full scan locally against all 48 fixtures plus the search-line and metadata-schema checks — everything passes, 99% coverage on the merged query.

@cx-artur-ribeiro cx-artur-ribeiro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the effort, I'll ask a second maintainer for review!
Insane job @cx-antero-silva 🙏

@Dashtid

Dashtid commented Oct 10, 2026

Copy link
Copy Markdown

is_loose_sub_value (^([^:]+:)?[*?], query.rego line 159 at 1a38276) only catches a wildcard at the start of the owner segment. A wildcard after a literal owner prefix counts as restrictive, so this gets no finding:

condition {
  test     = "StringLike"
  variable = "token.actions.githubusercontent.com:sub"
  values   = ["repo:octo-org*/octo-repo*:ref:refs/heads/main"]
}

StringLike wildcards can sit "anywhere in the string", so this admits repo:octo-org-evil/octo-repo-x:ref:refs/heads/main, and anyone can create an account or organisation named octo-org-evil. Like repo:?*/* in positive22.tf, it reaches owners the policy author does not control, which the org-scoped repo:myorg/* in negative7.tf does not.

The shape gets more likely after 2026-07-15. GitHub now issues repo:OWNER@OWNER_ID/REPO@REPO_ID:... subjects for new, renamed and transferred repositories, so repo:myorg/* stops matching an org's new repositories, and repo:myorg*/* is a one-value fix that accepts both formats.

A pattern like ^(([^:]+:)?|(repo|project_path):[^/:@]*)[*?] flags both shapes above and project_path:mygroup*/myproject:*, and changes no fixture verdict. Keeping the owner check to repo: and project_path: leaves Bitbucket's documented {REPO_UUID}:{*} clean.

A separate case goes the other way. is_oidc_trust accepts any Federated principal, so the trust policy AWS documents for a Cognito identity pool role gets the missing-sub finding. That policy has cognito-identity.amazonaws.com, a StringEquals on :aud and a ForAnyValue:StringLike on :amr, and no :sub, and its aud condition already pins the role to one identity pool.

Is a partial owner wildcard meant to be in scope, and should Cognito identity pools be outside the sub rule?

…pools

Two follow-up issues from review:

1. is_loose_sub_value only matched a wildcard immediately after the
   provider prefix (e.g. "repo:*"), missing a wildcard placed after a
   literal owner/group name (e.g. "repo:octo-org*/octo-repo*:..."),
   which still lets anyone able to register a matching org/repo name
   assume the role. Broadened the regex to also catch a wildcard
   anywhere in the owner segment when scoped to "repo:"/"project_path:"
   prefixes specifically, so Bitbucket's unrelated "{REPO_UUID}:{*}"
   stays unaffected. Verified against both regexes directly: the new
   pattern catches the bypass and leaves every existing fixture's
   verdict unchanged.

   Note: this also flags "repo:myorg*/*", which GitHub's 2026-07-15
   move to immutable "OWNER@OWNER_ID" subject claims makes a plausible
   one-org compatibility pattern going forward - the same shape as an
   actual overly-broad org-prefix match, and not distinguishable from
   it by pattern alone. Erring toward flagging here, consistent with
   this query's existing bias (e.g. the Not-operator and Null-operator
   exclusions already added).

2. is_oidc_trust matched any Federated principal, so AWS's documented
   Cognito Identity Pool trust pattern (Federated:
   cognito-identity.amazonaws.com, restricted via a StringEquals on
   ":aud", no ":sub" at all) was flagged as missing a sub condition.
   Cognito doesn't use ":sub" for this; added is_cognito_identity_pool
   to exclude it from is_oidc_trust entirely, since this query's
   premise doesn't apply to that trust model.

Adds positive30.tf (owner-prefix wildcard bypass) and
negative19.tf/negative21.tf (Cognito trust policy, JSON and HCL forms).
@cx-antero-silva

Copy link
Copy Markdown
Contributor Author

Both real, thanks — fixed in 02f55a6.

Cognito: yes, out of scope — added `is_cognito_identity_pool` to exclude it from `is_oidc_trust` entirely. AWS's documented Cognito pattern doesn't use `:sub` at all (it restricts via `:aud`/`:amr` instead), so this query's premise just doesn't apply to that trust model. Added JSON and HCL negative fixtures (negative19.tf/negative21.tf) using the exact pattern from the Cognito dev guide.

Partial owner wildcard: took your proposed regex as-is (verified it directly against Go's regexp/RE2, same engine Rego's regex.match uses) — repo:octo-org*/octo-repo*:... is now caught, and confirmed every existing fixture's verdict is unchanged (Bitbucket's {REPO_UUID}:{*} included). Added positive30.tf for the bypass.

On "is a partial owner wildcard meant to be in scope": I don't think we can resolve the myorg*/* ambiguity by pattern alone — it's structurally identical whether it's a deliberate one-org compatibility fix for the new OWNER@OWNER_ID subject format or an actual overly-broad match. I went with flagging it, mainly for consistency with how this query already treats other asymmetric tradeoffs here (e.g. excluding Null and Not-operators errs the same direction - toward catching more, accepting some legitimate-but-unusual patterns get flagged too). Noted this explicitly in the commit message in case that default is worth revisiting later - open to going the other way if you'd rather have myorg*/* pass quietly given the July change.

Ran the full local scan plus the metadata/content test suite - all 51 fixtures pass, 98% coverage.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(query): flag role + OIDC access misconfigurations between AWS and CI/CD

4 participants