-
Notifications
You must be signed in to change notification settings - Fork 26
feat: detect vulnerable GitHub Actions #1021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
3f5b70c
to
e056b7c
Compare
6dd85e0
to
6e429f7
Compare
Signed-off-by: behnazh-w <[email protected]>
6e429f7
to
9776ec8
Compare
@@ -46,12 +46,12 @@ Current checks in Macaron | |||
The table below shows the current set of actionable checks derived from | |||
the requirements that are currently supported by Macaron. | |||
|
|||
.. list-table:: Mapping SLSA requirements to Macaron checks | |||
.. list-table:: Macaron checks descriptions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.. list-table:: Macaron checks descriptions | |
.. list-table:: Macaron check descriptions |
How to detect vulnerable GitHub Actions | ||
======================================= | ||
|
||
This tutorial explains how to use a check in Macaron that detects vulnerable third-party GitHub Actions. This check is important for preventing security issues in your CI/CD pipeline, especially in light of recent incidents, such as vulnerabilities discovered in popular GitHub Actions like `tj-actions/changed-files <https://www.cve.org/CVERecord?id=CVE-2025-30066>`_, and `reviewdog/action-setup <https://www.cve.org/CVERecord?id=CVE-2025-30154>`_. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This tutorial explains how to use a check in Macaron that detects vulnerable third-party GitHub Actions. This check is important for preventing security issues in your CI/CD pipeline, especially in light of recent incidents, such as vulnerabilities discovered in popular GitHub Actions like `tj-actions/changed-files <https://www.cve.org/CVERecord?id=CVE-2025-30066>`_, and `reviewdog/action-setup <https://www.cve.org/CVERecord?id=CVE-2025-30154>`_. | |
This tutorial explains how to use a check in Macaron to detect vulnerable third-party GitHub Actions. This check is important for preventing security issues in your CI/CD pipeline, especially in light of recent incidents, such as vulnerabilities discovered in popular GitHub Actions: `tj-actions/changed-files <https://www.cve.org/CVERecord?id=CVE-2025-30066>`_, and `reviewdog/action-setup <https://www.cve.org/CVERecord?id=CVE-2025-30154>`_. |
|
||
This tutorial explains how to use a check in Macaron that detects vulnerable third-party GitHub Actions. This check is important for preventing security issues in your CI/CD pipeline, especially in light of recent incidents, such as vulnerabilities discovered in popular GitHub Actions like `tj-actions/changed-files <https://www.cve.org/CVERecord?id=CVE-2025-30066>`_, and `reviewdog/action-setup <https://www.cve.org/CVERecord?id=CVE-2025-30154>`_. | ||
|
||
We will guide you on how to enable and use this check to enhance the security of your development pipeline. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will guide you on how to enable and use this check to enhance the security of your development pipeline. | |
We will demonstrate how to enable and use this check to enhance the security of your development pipeline. |
if isinstance(callee, GitHubWorkflowNode) and callee.node_type in [ | ||
GitHubWorkflowType.EXTERNAL, | ||
GitHubWorkflowType.REUSABLE, | ||
]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if isinstance(callee, GitHubWorkflowNode) and callee.node_type in [ | |
GitHubWorkflowType.EXTERNAL, | |
GitHubWorkflowType.REUSABLE, | |
]: | |
if isinstance(callee, GitHubWorkflowNode) and callee.node_type in { | |
GitHubWorkflowType.EXTERNAL, | |
GitHubWorkflowType.REUSABLE, | |
}: |
if not workflow_name: | ||
logger.debug("Workflow %s is not relevant. Skipping...", callee.name) | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will only trigger if callee.name
has nothing before the @
symbol it contains. Is that expected in some cases?
CheckResultData | ||
The result of the check. | ||
""" | ||
result_tables: list[CheckFacts] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest moving this further down the function to just before where it's actually used. E.g. for vuln_res in batch_vulns:
section_name = "osv_dev" | ||
if not defaults.has_section(section_name): | ||
return [] | ||
section = defaults[section_name] | ||
|
||
url_netloc = section.get("url_netloc") | ||
if not url_netloc: | ||
raise APIAccessError( | ||
f'The "url_netloc" key is missing in section [{section_name}] of the .ini configuration file.' | ||
) | ||
url_scheme = section.get("url_scheme", "https") | ||
query_endpoint = section.get("querybatch_endpoint") | ||
if not query_endpoint: | ||
raise APIAccessError( | ||
f'The "query_endpoint" key is missing in section [{section_name}] of the .ini configuration file.' | ||
) | ||
try: | ||
url = urllib.parse.urlunsplit( | ||
urllib.parse.SplitResult( | ||
scheme=url_scheme, | ||
netloc=url_netloc, | ||
path=query_endpoint, | ||
query="", | ||
fragment="", | ||
) | ||
) | ||
except ValueError as error: | ||
raise APIAccessError("Failed to construct the API URL.") from error | ||
|
||
response = send_post_http_raw(url, json_data=query_data, headers=None) | ||
res_obj = None | ||
if response: | ||
try: | ||
res_obj = response.json() | ||
except requests.exceptions.JSONDecodeError as error: | ||
raise APIAccessError(f"Unable to get a valid response from {url}: {error}") from error | ||
|
||
results = res_obj.get("results") if res_obj else None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of this code is identical to call_osv_query_api
. I think we should be re-using it instead of duplicating it.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. | ||
|
||
description: | | ||
Analyzing with PURL and repository path without dependency resolution. | ||
Analyzing the staging branch of the Macaron repo to detect vulnerable GitHub Actions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test should be updated to use branch main
.
provenances=[], | ||
build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), | ||
) | ||
match ci_name: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason for using a match
statement here?
def test_is_affected_version_invalid_commit() -> None: | ||
"""Test if the function can handle invalid commits""" | ||
with pytest.raises(APIAccessError): | ||
OSVDevService.is_version_affected( | ||
vuln={}, pkg_name="pkg", pkg_version="invalid_commit", ecosystem="GitHub Actions" | ||
) | ||
|
||
|
||
def test_is_affected_version_invalid_response() -> None: | ||
"""Test if the function can handle empty OSV response.""" | ||
with pytest.raises(APIAccessError): | ||
OSVDevService.is_version_affected( | ||
vuln={"vulns": []}, pkg_name="repo/workflow", pkg_version="1.0.0", ecosystem="GitHub Actions" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me it seems that both of these functions fail because vuln
is not correctly populated. In is_version_affected
we have the following block:
affected = json_extract(vuln, ["affected"], list)
if not affected:
raise APIAccessError(f"Failed to extracted info for {pkg_name}@{pkg_version}.")
Can we be sure that the error is being raised from the expected part of the function?
@@ -1,4 +1,4 @@ | |||
.. Copyright (c) 2023 - 2023, Oracle and/or its affiliates. All rights reserved. | |||
.. Copyright (c) 2023 - 2025, Oracle and/or its affiliates. All rights reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, I don't think this file's copyright notice should be updated.
@@ -907,3 +907,79 @@ def is_empty_repo(git_obj: Git) -> bool: | |||
return False | |||
except GitCommandError: | |||
return True | |||
|
|||
|
|||
def is_commit_hash(version_str: str) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we rename this variable to make it more generic? From the doc string, I don't think it must be a version string.
except requests.exceptions.JSONDecodeError as error: | ||
raise APIAccessError(f"Unable to get a valid response from {url}: {error}") from error | ||
|
||
vulns = res_obj.get("vulns") if res_obj else None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we assume that the key vulns
always presents in res_obj
dictionary ?
if isinstance(results, list): | ||
if expected_size: | ||
if len(results) != expected_size: | ||
raise APIAccessError(f"Unable to get a valid result from {url}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we add the reason why this result is invalid (for example, saying that the results doesn't have the expected number of elements).
|
||
results = res_obj.get("results") if res_obj else None | ||
|
||
if isinstance(results, list): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If results
is not of type list, should we raise an exception instead ? Or we treat that as an "empty" result 🤔 .
pkg_version = tag | ||
break | ||
|
||
affected = json_extract(vuln, ["affected"], list) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we are assuming that affected
, if available, will never be an empty list right ?
workflow_name, | ||
workflow_inv["version"], | ||
"GitHub Actions", | ||
source_repo=f"https://github.com/{workflow_name}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use urllib
to construct the URL in this case (i.e. using urllib.parse.urlunsplit
) ? In this case, github.com
is a trusted domain so it wouldn't be a big problem, so both ways are okay to me.
Summary
This pull request introduces a new check
mcn_githubactions_vulnerabilities_1
to detect vulnerable GitHub Actions, enhancing the security of workflows and automating the identification of potential risks in CI/CD pipelines. The key changes include:Changes
Documentation
Tests